package de.jeisfeld.augendiagnoselib.components;
import android.app.Activity;
import android.app.FragmentManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import java.util.ArrayList;
import java.util.List;
import de.jeisfeld.augendiagnoselib.Application;
import de.jeisfeld.augendiagnoselib.R;
import de.jeisfeld.augendiagnoselib.fragments.DisplayImageFragment.OverlayStatus;
import de.jeisfeld.augendiagnoselib.util.PreferenceUtil;
import de.jeisfeld.augendiagnoselib.util.TrackingUtil;
import de.jeisfeld.augendiagnoselib.util.TrackingUtil.Category;
import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto;
import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft;
import de.jeisfeld.augendiagnoselib.util.imagefile.ImageUtil;
import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadata;
import de.jeisfeld.augendiagnoselib.util.imagefile.MediaStoreUtil;
import static de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView.Resolution.FULL;
import static de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView.Resolution.FULL_HIGH;
import static de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView.Resolution.HIGH;
import static de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView.Resolution.LOW;
/**
* Extension of PinchImageView which adds the Iristopography overlays to the view.
*
* @author Joerg
*/
public class OverlayPinchImageView extends PinchImageView {
/**
* The number of overlays (including circle and pupil overlays).
*/
public static final int OVERLAY_COUNT = Application.getAppContext().getResources().getIntArray(R.array.overlay_types).length;
/**
* The size of the overlays (in pixels).
*/
public static final int OVERLAY_SIZE = 1024;
/**
* The ratio of overlay circle diameter to overlay size.
*/
public static final float OVERLAY_CIRCLE_RATIO = 0.75f;
/**
* The pupil size used as default in display.
*/
public static final float DEFAULT_PUPIL_SIZE = Float.parseFloat(Application.getResourceString(R.string.overlay_default_pupil_size));
/**
* The index of the pupil overlay.
*/
public static final int OVERLAY_PUPIL_INDEX = OVERLAY_COUNT - 1;
/**
* The minimum scale factor allowed.
*/
private static final float MIN_OVERLAY_SCALE_FACTOR = 0.2f;
/**
* The maximum scale factor allowed.
*/
private static final float MAX_OVERLAY_SCALE_FACTOR = 5f;
/**
* The minimum pupil scale factor allowed.
*/
private static final float MIN_PUPIL_SCALE_FACTOR = 0.05f;
/**
* The maximum pupil scale factor allowed.
*/
private static final float MAX_PUPIL_SCALE_FACTOR = 0.9f;
/**
* The limiting value of contrast (must ensure that offset is smaller than 2^15).
*/
private static final float CONTRAST_LIMIT = 0.98f;
/**
* The color of the one-colored overlays.
*/
private int mOverlayColor = Color.RED;
/**
* An array of the available overlays.
*/
@NonNull
private Drawable[] mOverlayCache = new Drawable[OVERLAY_COUNT];
/**
* These are the relative positions of the overlay center on the bitmap. Range: [0,1]
*/
private float mOverlayX, mOverlayY;
/**
* The scale factor of the overlays. Value 1 means that one overlay image pixel corresponds to one base image pixel.
*/
private float mOverlayScaleFactor, mLastOverlayScaleFactor;
/**
* These are the positions of the pupil overlay center relative to the iris. Range: [-0.5,0.5]
*/
private float mPupilOverlayX, mPupilOverlayY;
/**
* The scale factor of the pupil overlays (as relative size compared to the main overlay).
*/
private float mPupilOverlayScaleFactor, mLastPupilOverlayScaleFactor;
/**
* Flag indicating if the pupil has been changed.
*/
private boolean mIsPupilChanged = false;
/**
* An array indicating which overlays are displayed.
*/
@Nullable
private boolean[] mShowOverlay = new boolean[OVERLAY_COUNT];
/**
* Flag indicating if the overlays are locked.
*/
private boolean mLocked = false;
/**
* The way in which pinching is done.
*/
private PinchMode mPinchMode = PinchMode.ALL;
/**
* The eye photo displayed.
*/
private EyePhoto mEyePhoto;
/**
* The bitmap drawn on the canvas.
*/
private Bitmap mCanvasBitmap;
/**
* The canvas on which the drawing is done.
*/
private Canvas mCanvas;
/**
* The brightness (-1 to 1) of the bitmap. Default: 0.
*/
private float mBrightness = 0f;
/**
* The contrast (0 to infinity) of the bitmap. Default: 1.
*/
private float mContrast = 1f;
/**
* The saturation (1/3 to infinity) of the bitmap. Default: 1.
*/
private float mSaturation = 1f;
/**
* The color temperature (-1 to 1) of the bitmap.
*/
private float mColorTemperature = 0f;
/**
* A small version of the bitmap.
*/
private Bitmap mBitmapSmall;
/**
* The partial bitmap with full resolution.
*/
private Bitmap mPartialBitmapFullResolution;
/**
* The partial bitmap with full resolution, including brightness.
*/
private Bitmap mPartialBitmapFullResolutionWithBrightness;
/**
* Flag indicating if currently the full resolution snapshot is displayed.
*/
private boolean mShowingFullResolution = false;
/**
* The full bitmap (full resolution).
*/
private Bitmap mBitmapFull = null;
/**
* The metadata of the image.
*/
private JpegMetadata mMetadata;
/**
* Flag indicating if the overlay position is defined for the eye photo.
*/
private boolean mHasOverlayPosition = false;
/**
* Flag indicating if the view position is stored for the eye photo.
*/
private boolean mHasViewPosition = false;
/**
* Flag indicating if brightness/contrast is changed, but not yet applied to mBitmap.
*/
private boolean mNeedsBitmapRefresh = false;
/**
* Last height of the view. Used to make sure that full resolution is abandoned as soon as view size changes.
*/
private int mLastHeight;
/**
* Last width of the view. Used to make sure that full resolution is abandoned as soon as view size changes.
*/
private int mLastWidth;
/**
* Thread showing the view in full resolution. The first is the running thread. Another one may be queuing.
*/
private final List<Thread> mFullResolutionThreads = new ArrayList<>();
/**
* Callback class to update the GUI elements from the view.
*/
private GuiElementUpdater mGuiElementUpdater;
/**
* A String indicating if full resolution image should be automatically loaded or even kept in memory.
*/
private boolean mFullResolutionFlag;
/**
* The retain fragment.
*/
private RetainFragment mRetainFragment;
/**
* Standard constructor to be implemented for all views.
*
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
* @see android.view.View#View(Context)
*/
public OverlayPinchImageView(final Context context) {
this(context, null, 0);
}
/**
* Standard constructor to be implemented for all views.
*
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @see android.view.View#View(Context, AttributeSet)
*/
public OverlayPinchImageView(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Standard constructor to be implemented for all views.
*
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyle An attribute in the current theme that contains a reference to a style resource that supplies default
* values for the view. Can be 0 to not look for defaults.
* @see android.view.View#View(Context, AttributeSet, int)
*/
public OverlayPinchImageView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
/**
* Get the EyePhoto displayed in the view.
*
* @return the EyePhoto
*/
public final EyePhoto getEyePhoto() {
return mEyePhoto;
}
/**
* Fill with an image, initializing from metadata.
*
* @param pathName The pathname of the image
* @param activity The triggering activity (required for bitmap caching)
* @param cacheIndex A unique index of the view in the activity
*/
@Override
public final void setImage(@NonNull final String pathName, @NonNull final Activity activity, final int cacheIndex) {
mEyePhoto = new EyePhoto(pathName);
final RetainFragment retainFragment = RetainFragment.findOrCreateRetainFragment(activity.getFragmentManager(),
cacheIndex);
mRetainFragment = retainFragment;
mBitmap = retainFragment.getBitmap();
mBitmapSmall = retainFragment.getBitmapSmall();
mBitmapFull = retainFragment.getBitmapFullResolution();
cleanFullResolutionBitmaps(false);
if (mBitmap == null || !pathName.equals(mPathName)) {
mHasOverlayPosition = false;
mPathName = pathName;
mBitmap = null;
// Do image loading in separate thread
Thread thread = new Thread() {
@Override
public void run() {
mBitmap = mEyePhoto.getImageBitmap(mMaxBitmapSize);
mBitmapSmall = mEyePhoto.getImageBitmap(MediaStoreUtil.MINI_THUMB_SIZE);
mMetadata = mEyePhoto.getImageMetadata();
retainFragment.setBitmap(mBitmap);
retainFragment.setBitmapSmall(mBitmapSmall);
mIsBitmapSet = true;
post(new Runnable() {
@Override
public void run() {
if (mMetadata != null && mMetadata.hasOverlayPosition()) {
// stored position of overlay
mHasOverlayPosition = true;
mOverlayX = mMetadata.getXCenter();
mOverlayY = mMetadata.getYCenter();
mOverlayScaleFactor = mMetadata.getOverlayScaleFactor()
* Math.max(mBitmap.getHeight(), mBitmap.getWidth()) / OVERLAY_SIZE;
boolean shouldBeLocked = !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY)
&& !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY);
lockOverlay(shouldBeLocked, false);
if (mGuiElementUpdater != null) {
mGuiElementUpdater.setLockChecked(shouldBeLocked);
}
if (mMetadata.getPupilSize() == null) {
mPupilOverlayScaleFactor = DEFAULT_PUPIL_SIZE;
}
else {
mPupilOverlayScaleFactor = mMetadata.getPupilSize();
}
if (mMetadata.getPupilXOffset() == null || mMetadata.getPupilYOffset() == null) {
mPupilOverlayX = 0;
mPupilOverlayY = 0;
}
else {
mPupilOverlayX = mMetadata.getPupilXOffset();
mPupilOverlayY = mMetadata.getPupilYOffset();
}
}
else {
// initial position of overlay
resetOverlayPosition(false);
}
if (mMetadata != null && mMetadata.hasViewPosition()) {
mHasViewPosition = true;
}
if (mMetadata != null && mMetadata.hasBrightnessContrast()) {
mBrightness = mMetadata.getBrightness();
mContrast = mMetadata.getContrast();
mSaturation = mMetadata.getSaturation() == null ? 1f : mMetadata.getSaturation();
mColorTemperature = mMetadata.getColorTemperature() == null ? 0f : mMetadata.getColorTemperature();
if (mGuiElementUpdater != null) {
mGuiElementUpdater.updateSeekbarBrightness(mBrightness);
mGuiElementUpdater.updateSeekbarContrast(storedContrastToSeekbarContrast(mContrast));
mGuiElementUpdater.updateSeekbarSaturation(storedSaturationToSeekbarSaturation(mSaturation));
mGuiElementUpdater.updateSeekbarColorTemperature(mColorTemperature);
}
}
if (mMetadata != null && mMetadata.getOverlayColor() != null && mGuiElementUpdater != null) {
mOverlayColor = mMetadata.getOverlayColor();
mGuiElementUpdater.updateOverlayColorButton(mOverlayColor);
}
mLastOverlayScaleFactor = mOverlayScaleFactor;
mLastPupilOverlayScaleFactor = mPupilOverlayScaleFactor;
mCanvasBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mCanvasBitmap);
doInitialScaling();
updatePinchMode();
refresh(HIGH);
showFullResolutionSnapshot(true);
}
});
}
};
thread.start();
}
else {
// orientation change
mMetadata = mEyePhoto.getImageMetadata();
mHasOverlayPosition = mMetadata != null && mMetadata.hasOverlayPosition();
mIsBitmapSet = true;
mCanvasBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mCanvasBitmap);
doInitialScaling();
updatePinchMode();
refresh(HIGH);
showFullResolutionSnapshot(true);
// Update lock status - required in the case that orientation change happened while loading image.
if (mMetadata != null && mMetadata.hasOverlayPosition() && mGuiElementUpdater != null // BOOLEAN_EXPRESSION_COMPLEXITY
&& !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY)
&& !mMetadata.hasFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY)) {
mGuiElementUpdater.setLockChecked(true);
}
}
}
@Override
protected final void doInitialScaling() {
// If available, use stored position
if (!mInitialized && mHasViewPosition) {
mPosX = mMetadata.getXPosition();
mPosY = mMetadata.getYPosition();
mScaleFactor = mMetadata.getZoomFactor() * getOrientationIndependentScaleFactor();
mLastScaleFactor = mScaleFactor;
mInitialized = true;
}
// Otherwise, if available, use overlay position
if (!mInitialized && mHasOverlayPosition) {
mPosX = mOverlayX;
mPosY = mOverlayY;
mScaleFactor = 1f;
if (getHeight() > 0 && getWidth() > 0) {
final float size = Math.min(getHeight(), getWidth());
mScaleFactor = size / (OVERLAY_SIZE * mOverlayScaleFactor);
mInitialized = true;
}
mLastScaleFactor = mScaleFactor;
}
// Otherwise, use default (set if mInitialized = false)
super.doInitialScaling();
resetOverlayCache();
cleanFullResolutionBitmaps(false);
}
/**
* Reapply the initial scaling of the image.
*/
public final void redoInitialScaling() {
mInitialized = false;
doInitialScaling();
}
/**
* Update the bitmap with the correct set of overlays.
*
* @param resolution indicates what resolution is required
*/
private void refresh(final Resolution resolution) {
if (mCanvas == null || !mInitialized) {
return;
}
if (resolution == FULL) {
showFullResolutionSnapshot(true);
return;
}
else if (resolution == HIGH || resolution == LOW) {
interruptFullResolutionThread();
}
// Determine overlays to be shown
List<Integer> overlayPositions = getOverlayPositions();
Drawable[] layers = new Drawable[overlayPositions.size() + 1];
Bitmap modBitmap;
// Even in full resolution, first calculate high resolution image.
if (resolution == LOW) {
// for performance reasons, use only low resolution bitmap while pinching
modBitmap = ImageUtil.changeBitmapColors(mBitmapSmall, mContrast, mBrightness, mSaturation, mColorTemperature);
}
else {
modBitmap = ImageUtil.changeBitmapColors(mBitmap, mContrast, mBrightness, mSaturation, mColorTemperature);
}
layers[0] = new BitmapDrawable(getResources(), modBitmap);
for (int i = 0; i < overlayPositions.size(); i++) {
layers[i + 1] = getOverlayDrawable(overlayPositions.get(i));
}
LayerDrawable layerDrawable = new LayerDrawable(layers);
int width = mBitmap.getWidth();
int height = mBitmap.getHeight();
// position overlays
for (int i = 1; i < layerDrawable.getNumberOfLayers(); i++) {
boolean isPupil = overlayPositions.get(i - 1) == OVERLAY_PUPIL_INDEX;
if (isPupil) {
float totalPupilOverlayScaleFactor = mPupilOverlayScaleFactor * mOverlayScaleFactor * OVERLAY_SIZE / 2;
float overlayAbsoluteSize = mOverlayScaleFactor * OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO;
layerDrawable.setLayerInset(i,
(int) (mPupilOverlayX * overlayAbsoluteSize + mOverlayX * width - totalPupilOverlayScaleFactor),
(int) (mPupilOverlayY * overlayAbsoluteSize + mOverlayY * height - totalPupilOverlayScaleFactor),
(int) (width - mPupilOverlayX * overlayAbsoluteSize - mOverlayX * width - totalPupilOverlayScaleFactor),
(int) (height - mPupilOverlayY * overlayAbsoluteSize - mOverlayY * height - totalPupilOverlayScaleFactor));
}
else {
layerDrawable.setLayerInset(i, (int) (mOverlayX * width - OVERLAY_SIZE / 2 * mOverlayScaleFactor),
(int) (mOverlayY * height - OVERLAY_SIZE / 2 * mOverlayScaleFactor),
(int) (width - mOverlayX * width - OVERLAY_SIZE / 2 * mOverlayScaleFactor),
(int) (height - mOverlayY * height - OVERLAY_SIZE / 2 * mOverlayScaleFactor));
}
}
layerDrawable.setBounds(0, 0, width, height);
mCanvas.drawColor(Color.BLACK);
layerDrawable.draw(mCanvas);
if (resolution == FULL_HIGH) {
showFullResolutionSnapshot(true);
}
else if (mPartialBitmapFullResolution == null) {
setImageBitmap(mCanvasBitmap);
super.setMatrix();
invalidate();
}
else {
cleanFullResolutionBitmaps(false);
}
mNeedsBitmapRefresh = false;
}
/**
* Refresh with high resolution (or full resolution if applicable).
*/
public final void refresh() {
refresh(mFullResolutionFlag ? FULL_HIGH : HIGH);
}
/**
* Get the list of currently displayed overlay indices.
*
* @return The current overlay indices.
*/
@NonNull
private List<Integer> getOverlayPositions() {
ArrayList<Integer> overlayPositions = new ArrayList<>();
if (canHandleOverlays()) {
for (int i = 0; i < mShowOverlay.length; i++) {
if (mShowOverlay[i]) {
overlayPositions.add(i);
}
}
}
return overlayPositions;
}
/**
* Get information if the view can handle overlays.
*
* @return true if the view can handle overlays. This is possible only if the right/left position of the eye photo
* is defined.
*/
public final boolean canHandleOverlays() {
return mEyePhoto != null && mEyePhoto.getRightLeft() != null;
}
/**
* Trigger one overlay either for activation or for deactivation.
*
* @param position number of the overlay
* @param pinchMode the way in which pinching should be done. ALL indicates that the overlay should not be shown.
*/
public final void triggerOverlay(final int position, final PinchMode pinchMode) {
for (int i = 0; i < mShowOverlay.length; i++) {
mShowOverlay[i] = i == position && pinchMode != PinchMode.ALL;
}
mPinchMode = pinchMode;
updatePinchMode();
mNeedsBitmapRefresh = true;
refresh();
}
/**
* Switch the lock status of the overlays.
*
* @param lock the target lock status
* @param store a flag indicating if the lock status should be stored.
*/
public final void lockOverlay(final boolean lock, final boolean store) {
this.mLocked = lock;
if (lock && store && mInitialized) {
if (mMetadata != null) {
if (mMetadata.getRightLeft() == null) {
// If image did not yet pass metadata setting, do it now.
mEyePhoto.updateMetadataWithDefaults(mMetadata);
}
mMetadata.setXCenter(mOverlayX);
mMetadata.setYCenter(mOverlayY);
mMetadata.setOverlayScaleFactor(mOverlayScaleFactor / Math.max(mBitmap.getWidth(), mBitmap.getHeight()) * OVERLAY_SIZE);
mMetadata.removeFlag(JpegMetadata.FLAG_OVERLAY_SET_BY_CAMERA_ACTIVITY);
mEyePhoto.storeImageMetadata(mMetadata);
mHasOverlayPosition = true;
PreferenceUtil.incrementCounter(R.string.key_statistics_countlock);
TrackingUtil.sendEvent(Category.EVENT_USER, "Lock iris position", null);
}
}
updatePinchMode();
}
/**
* Change the positioning of the image dependent on the overlay setup phase.
*
* @param overlayStatus The overlay status.
* @param circleRadius The relative circle radius (compared to min view dimension)
*/
public final void updatePosition(final OverlayStatus overlayStatus, final float circleRadius) {
switch (overlayStatus) {
case GUIDE_IRIS:
if (mMetadata == null || !mMetadata.hasOverlayPosition()) {
return;
}
mOverlayX = mMetadata.getXCenter();
mOverlayY = mMetadata.getYCenter();
mOverlayScaleFactor = mMetadata.getOverlayScaleFactor() * Math.max(mBitmap.getHeight(), mBitmap.getWidth()) / OVERLAY_SIZE;
mPosX = mOverlayX;
mPosY = mOverlayY;
float bitmapPixelDiameter = mOverlayScaleFactor * OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO;
mScaleFactor = Math.min(getWidth(), getHeight()) * 2 * circleRadius / bitmapPixelDiameter;
mLastScaleFactor = mScaleFactor;
interruptFullResolutionThread();
refresh();
break;
case GUIDE_PUPIL:
if (mMetadata == null || mMetadata.getPupilSize() == null || mMetadata.getPupilSize() == 0) {
return;
}
mPupilOverlayX = mMetadata.getPupilXOffset();
mPupilOverlayY = mMetadata.getPupilYOffset();
mPupilOverlayScaleFactor = mMetadata.getPupilSize();
float overlaySizeOnBitmap = OVERLAY_CIRCLE_RATIO * mMetadata.getOverlayScaleFactor() * Math.max(mBitmap.getHeight(), mBitmap.getWidth());
mPosX = mPupilOverlayX * overlaySizeOnBitmap / mBitmap.getWidth() + mOverlayX;
mPosY = mPupilOverlayY * overlaySizeOnBitmap / mBitmap.getHeight() + mOverlayY;
float bitmapPixelDiameter2 = mPupilOverlayScaleFactor * overlaySizeOnBitmap;
mScaleFactor = Math.min(getWidth(), getHeight()) * 2 * circleRadius / bitmapPixelDiameter2;
mLastScaleFactor = mScaleFactor;
interruptFullResolutionThread();
refresh();
break;
case ALLOWED:
mInitialized = false;
doInitialScaling();
break;
default:
break;
}
}
/**
* Set the overlay position, so that it matches a centered circle.
*
* @param circleRadius The relative circle radius (compared to min view dimension)
*/
public final void setOverlayPosition(final float circleRadius) {
mOverlayX = mPosX;
mOverlayY = mPosY;
float bitmapPixelDiameter = Math.min(getWidth(), getHeight()) * 2 * circleRadius / mScaleFactor;
mOverlayScaleFactor = bitmapPixelDiameter / (OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO);
}
/**
* Store the pupil position in the metadata, if changed.
*/
public final void storePupilPosition() {
if (mIsPupilChanged && mMetadata != null && mMetadata.hasOverlayPosition()) {
mMetadata.setPupilSize(mPupilOverlayScaleFactor);
mMetadata.setPupilXOffset(mPupilOverlayX);
mMetadata.setPupilYOffset(mPupilOverlayY);
mMetadata.removeFlag(JpegMetadata.FLAG_OVERLAY_POSITION_DETERMINED_AUTOMATICALLY);
mEyePhoto.storeImageMetadata(mMetadata);
resetOverlayCache();
mIsPupilChanged = false;
}
}
/**
* Set the pupil position, so that it matches a centered circle.
*
* @param circleRadius The relative circle radius (compared to min view dimension)
*/
public final void setPupilPosition(final float circleRadius) {
float overlaySizeOnBitmap = OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO * mOverlayScaleFactor;
mPupilOverlayX = (mPosX - mOverlayX) * mBitmap.getWidth() / overlaySizeOnBitmap;
mPupilOverlayY = (mPosY - mOverlayY) * mBitmap.getHeight() / overlaySizeOnBitmap;
float bitmapPixelDiameter = Math.min(getWidth(), getHeight()) * 2 * circleRadius / mScaleFactor;
mPupilOverlayScaleFactor = bitmapPixelDiameter / overlaySizeOnBitmap;
// ensure boundary conditions
if (mPupilOverlayScaleFactor > MAX_PUPIL_SCALE_FACTOR) {
mPupilOverlayScaleFactor = MAX_PUPIL_SCALE_FACTOR;
}
else if (mPupilOverlayScaleFactor < MIN_PUPIL_SCALE_FACTOR) {
mPupilOverlayScaleFactor = MIN_PUPIL_SCALE_FACTOR;
}
ensureProperPupilOffsets();
mIsPupilChanged = true;
}
/**
* Ensure that the pupil overlay offsets are within allowed bounds.
*/
private void ensureProperPupilOffsets() {
float maxOffsetSquared = (1 - mPupilOverlayScaleFactor) * (1 - mPupilOverlayScaleFactor) / 4; // MAGIC_NUMBER
float currentOffsetSquared = mPupilOverlayX * mPupilOverlayX + mPupilOverlayY * mPupilOverlayY;
if (currentOffsetSquared > maxOffsetSquared) {
float correctionFactor = (float) Math.sqrt(maxOffsetSquared / currentOffsetSquared);
mPupilOverlayX = mPupilOverlayX * correctionFactor;
mPupilOverlayY = mPupilOverlayY * correctionFactor;
}
}
/**
* Reset the overlay cache.
*/
private void resetOverlayCache() {
mOverlayCache = new Drawable[OVERLAY_COUNT];
}
/**
* Reset the overlay position.
*
* @param store flag indicating if the overlay position should be stored.
*/
public final void resetOverlayPosition(final boolean store) {
float size = Math.min(mBitmap.getWidth(), mBitmap.getHeight());
mOverlayScaleFactor = size / OVERLAY_SIZE;
mPupilOverlayScaleFactor = DEFAULT_PUPIL_SIZE;
mOverlayX = ONE_HALF;
mOverlayY = ONE_HALF;
mPupilOverlayX = 0;
mPupilOverlayY = 0;
if (store && mInitialized) {
if (mMetadata != null) {
mMetadata.setXCenter((Float) null);
mMetadata.setYCenter((Float) null);
mMetadata.setOverlayScaleFactor((Float) null);
mMetadata.setPupilSize((Float) null);
mMetadata.setPupilXOffset((Float) null);
mMetadata.setPupilYOffset((Float) null);
mEyePhoto.storeImageMetadata(mMetadata);
mHasOverlayPosition = false;
}
}
mLocked = false;
for (int i = 0; i < OVERLAY_COUNT; i++) {
mShowOverlay[i] = false;
}
updatePinchMode();
if (mGuiElementUpdater != null) {
mGuiElementUpdater.setLockChecked(false);
mGuiElementUpdater.resetOverlays();
}
refresh();
}
/**
* Set the correct ScaleGestureDetector.
*/
private void updatePinchMode() {
mPinchMode = determinePinchMode();
if (mPinchMode == PinchMode.ALL) {
mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
else if (mPinchMode == PinchMode.OVERLAY) {
mScaleDetector = new ScaleGestureDetector(getContext(), new OverlayScaleListener());
}
else {
mScaleDetector = new ScaleGestureDetector(getContext(), new PupilOverlayScaleListener());
}
}
/**
* Helper method to create the overlay drawable of position i.
*
* @param position The position of the overlay drawable.
* @return The overlay drawable.
*/
private Drawable getOverlayDrawable(final int position) {
Drawable overlayDrawable = mOverlayCache[position];
if (overlayDrawable == null) {
int[] overlayTypes = getResources().getIntArray(R.array.overlay_types);
TypedArray overlaysLeft = getResources().obtainTypedArray(R.array.overlays_left);
TypedArray overlaysRight = getResources().obtainTypedArray(R.array.overlays_right);
if (position < overlayTypes.length) {
String origPupilSizeString = getResources().getStringArray(R.array.overlay_pupil_sizes)[position];
float origPupilSize = Float.parseFloat(origPupilSizeString);
Drawable drawable;
if (mEyePhoto.getRightLeft().equals(RightLeft.RIGHT)) {
drawable = overlaysRight.getDrawable(position);
}
else {
drawable = overlaysLeft.getDrawable(position);
}
Integer targetColor = overlayTypes[position] == 1 ? mOverlayColor : null;
if (mMetadata == null) {
overlayDrawable = getModifiedDrawable(drawable, targetColor, origPupilSize, DEFAULT_PUPIL_SIZE, 0f, 0f);
}
else {
overlayDrawable = getModifiedDrawable(drawable, targetColor, origPupilSize, mMetadata.getPupilSize(),
mMetadata.getPupilXOffset(), mMetadata.getPupilYOffset());
}
mOverlayCache[position] = overlayDrawable;
}
overlaysLeft.recycle();
overlaysRight.recycle();
}
return overlayDrawable;
}
/**
* Create a drawable from a black image drawable, having a changed colour.
*
* @param sourceDrawable The black image drawable
* @param color The target color
* @param origPupilSize The pupil size (relative to iris) in the original overlay bitmap.
* @param destPupilSize The pupil size (relative to iris) in the target overlay bitmap.
* @param pupilOffsetX The relative x offset of the pupil center
* @param pupilOffsetY The relative y offset of the pupil center
* @return The modified drawable, with the intended color.
*/
@NonNull
private Drawable getModifiedDrawable(@NonNull final Drawable sourceDrawable, @Nullable final Integer color,
final float origPupilSize, @Nullable final Float destPupilSize,
final Float pupilOffsetX, final Float pupilOffsetY) {
Bitmap bitmap = ((BitmapDrawable) sourceDrawable).getBitmap();
Bitmap colouredBitmap = color == null ? bitmap : ImageUtil.changeBitmapColor(bitmap, color);
float targetPupilSize = destPupilSize == null ? DEFAULT_PUPIL_SIZE : destPupilSize;
Bitmap deformedBitmap = ImageUtil.deformOverlayByPupilSize(colouredBitmap, origPupilSize, targetPupilSize, pupilOffsetX, pupilOffsetY);
return new BitmapDrawable(getResources(), deformedBitmap);
}
/**
* Utility method to determine the pinch mode.
*
* @return the pinch mode.
*/
@Nullable
private PinchMode determinePinchMode() {
if (mPinchMode == PinchMode.PUPIL || mPinchMode == PinchMode.PUPIL_CENTER) {
// do not update pupil pinch modes implicitly.
return mPinchMode;
}
if (mLocked) {
return PinchMode.ALL;
}
int overlayCount = 0;
for (boolean b : mShowOverlay) {
if (b) {
overlayCount++;
}
}
return overlayCount == 0 ? PinchMode.ALL : PinchMode.OVERLAY;
}
/**
* Update color settings (brightness, contrast, saturation, colorTemperature) of the image.
*
* @param brightness the brightness on a scale -1 to 1
* @param contrast the contrast on a scale from -1 to 1
* @param saturation the saturation on a scale from -1 to 1.
* @param colorTemperature the color temperature on a scale from -1 to 1.
* @param fromSeekbar flag indicating if the color change was triggered from a move on the seekbar.
*/
public final void updateColorSettings(final Float brightness, final Float contrast, final Float saturation, final Float colorTemperature,
final boolean fromSeekbar) {
if (brightness != null) {
mBrightness = brightness;
}
if (contrast != null) {
mContrast = seekbarContrastToStoredContrast(contrast);
}
if (saturation != null) {
mSaturation = seekbarSaturationToStoredSaturation(saturation); // MAGIC_NUMBER
}
if (colorTemperature != null) {
mColorTemperature = colorTemperature;
}
mNeedsBitmapRefresh = true;
cleanFullResolutionBitmaps(true);
if (fromSeekbar) {
refresh(mPartialBitmapFullResolution == null ? LOW : FULL);
}
else {
refresh();
}
}
/**
* Set the overlay color.
*
* @param overlayColor the overlay color (such as Color.RED)
*/
public final void setOverlayColor(final int overlayColor) {
mOverlayColor = overlayColor;
mNeedsBitmapRefresh = true;
resetOverlayCache();
mGuiElementUpdater.updateOverlayColorButton(overlayColor);
if (getOverlayPositions().size() > 0) {
refresh();
}
}
/**
* Get the overlay color.
*
* @return the overlay color (such as Color.RED)
*/
public final int getOverlayColor() {
return mOverlayColor;
}
/**
* Store brightness and contrast in the image metadata.
*
* @param delete delete brightness and contrast from metadata.
*/
public final void storeColorSettings(final boolean delete) {
if (mInitialized && mMetadata != null) {
if (delete) {
mMetadata.setBrightness((Float) null);
mMetadata.setContrast((Float) null);
mMetadata.setSaturation((Float) null);
mMetadata.setColorTemperature((Float) null);
mNeedsBitmapRefresh = true;
cleanFullResolutionBitmaps(true);
mBrightness = 0;
mContrast = 1;
mSaturation = 1;
mColorTemperature = 0;
if (mGuiElementUpdater != null) {
mGuiElementUpdater.updateSeekbarBrightness(mBrightness);
mGuiElementUpdater.updateSeekbarContrast(storedContrastToSeekbarContrast(mContrast));
mGuiElementUpdater.updateSeekbarSaturation(storedSaturationToSeekbarSaturation(mSaturation));
mGuiElementUpdater.updateSeekbarColorTemperature(mColorTemperature);
}
refresh();
}
else {
mMetadata.setBrightness(mBrightness);
mMetadata.setContrast(mContrast);
mMetadata.setSaturation(mSaturation);
mMetadata.setColorTemperature(mColorTemperature);
}
mEyePhoto.storeImageMetadata(mMetadata);
}
}
/**
* Convert contrast from (-1,1) scale to (0,infty) scale.
*
* @param seekbarContrast the contrast on (-1,1) scale.
* @return the contrast on (0,infty) scale.
*/
private static float seekbarContrastToStoredContrast(final float seekbarContrast) {
float contrastImd = (float) (Math.asin(seekbarContrast) * 2 / Math.PI);
return 2f / (1f - contrastImd * CONTRAST_LIMIT) - 1f;
}
/**
* Convert contrast from (0,infty) scale to (-1,1) scale.
*
* @param storedContrast the contrast on (0,infty) scale.
* @return the contrast on (-1,1) scale.
*/
private static float storedContrastToSeekbarContrast(final float storedContrast) {
float contrastImd = (1f - 2f / (storedContrast + 1f)) / CONTRAST_LIMIT;
return (float) Math.sin(Math.PI * contrastImd / 2);
}
/**
* Convert saturation from (-1,1) scale to (1/3,infty) scale.
*
* @param seekbarSaturation the saturation on (-1,1) scale.
* @return the saturation on (1/3,infty) scale.
*/
private static float seekbarSaturationToStoredSaturation(final float seekbarSaturation) {
return 4f / 3 / (1f - seekbarSaturation * CONTRAST_LIMIT) - 1f / 3; // MAGIC_NUMBER
}
/**
* Convert saturation from (0,infty) scale to (-1,1) scale.
*
* @param storedSaturation the saturation on (0,infty) scale.
* @return the saturation on (-1,1) scale.
*/
private static float storedSaturationToSeekbarSaturation(final float storedSaturation) {
return (1f - 4f / 3 / (storedSaturation + 1f / 3)) / CONTRAST_LIMIT; // MAGIC_NUMBER
}
/**
* Store position and zoom in the image metadata.
*
* @param delete delete position and zoom from metadata.
*/
public final void storePositionZoom(final boolean delete) {
if (mInitialized && mMetadata != null) {
if (delete) {
mHasViewPosition = false;
mMetadata.setXPosition((Float) null);
mMetadata.setYPosition((Float) null);
mMetadata.setZoomFactor((Float) null);
// Reset to original view size
mInitialized = false;
doInitialScaling();
refresh();
}
else {
mHasViewPosition = true;
mMetadata.setXPosition(mPosX);
mMetadata.setYPosition(mPosY);
mMetadata.setZoomFactor(mScaleFactor / getOrientationIndependentScaleFactor());
}
mEyePhoto.storeImageMetadata(mMetadata);
}
}
/**
* Store the overlay color in the image metadata.
*
* @param delete delete the overlay color from metadata.
*/
public final void storeOverlayColor(final boolean delete) {
if (mInitialized && mMetadata != null) {
if (delete) {
mMetadata.setOverlayColor((Integer) null);
setOverlayColor(mGuiElementUpdater.getOverlayDefaultColor());
}
else {
mMetadata.setOverlayColor(mOverlayColor);
}
mEyePhoto.storeImageMetadata(mMetadata);
}
}
/*
* Utility method to make the calculations in case of a pointer move Overridden to handle zooming of overlay.
*/
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY",
justification = "Using floating point equality to see if value has changed")
@Override
protected final boolean handlePointerMove(@NonNull final MotionEvent ev) {
if (mPinchMode == PinchMode.ALL) {
cleanFullResolutionBitmaps(false);
return super.handlePointerMove(ev);
}
else if (mPinchMode == PinchMode.PUPIL_CENTER) {
if (mPupilOverlayScaleFactor == mLastPupilOverlayScaleFactor) {
return false;
}
else {
mLastPupilOverlayScaleFactor = mPupilOverlayScaleFactor;
mPupilOverlayX = 0;
mPupilOverlayY = 0;
mIsPupilChanged = true;
refresh(mFullResolutionFlag ? FULL : LOW);
return true;
}
}
boolean moved = false;
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
if (mPinchMode == PinchMode.PUPIL) {
float overlayAbsoluteSize = mScaleFactor * mOverlayScaleFactor * OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO;
if (mActivePointerId2 == INVALID_POINTER_ID) {
// Only move if the ScaleGestureDetector isn't processing a gesture.
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mPupilOverlayX += dx / overlayAbsoluteSize;
mPupilOverlayY += dy / overlayAbsoluteSize;
}
else {
// When resizing, move according to the center of the two pinch points
final int pointerIndex2 = ev.findPointerIndex(mActivePointerId2);
final float x0 = (ev.getX(pointerIndex2) + x) / 2;
final float y0 = (ev.getY(pointerIndex2) + y) / 2;
final float dx = x0 - mLastTouchX0;
final float dy = y0 - mLastTouchY0;
mPupilOverlayX += dx / overlayAbsoluteSize;
mPupilOverlayY += dy / overlayAbsoluteSize;
if (mPupilOverlayScaleFactor != mLastPupilOverlayScaleFactor) {
// When resizing, then position also changes
final float changeFactor = mPupilOverlayScaleFactor / mLastPupilOverlayScaleFactor;
final float pinchX =
(x0 - (float) getWidth() / 2) / overlayAbsoluteSize + mPosX * mBitmap.getWidth() * mScaleFactor / overlayAbsoluteSize;
final float pinchY =
(y0 - (float) getHeight() / 2) / overlayAbsoluteSize + mPosY * mBitmap.getHeight() * mScaleFactor / overlayAbsoluteSize;
mPupilOverlayX = pinchX + (mPupilOverlayX - pinchX) * changeFactor
+ mOverlayX * (changeFactor - 1) * mBitmap.getWidth() * mScaleFactor / overlayAbsoluteSize;
mPupilOverlayY = pinchY + (mPupilOverlayY - pinchY) * changeFactor
+ mOverlayY * (changeFactor - 1) * mBitmap.getHeight() * mScaleFactor / overlayAbsoluteSize;
mLastPupilOverlayScaleFactor = mPupilOverlayScaleFactor;
moved = true;
}
mLastTouchX0 = x0;
mLastTouchY0 = y0;
}
if (x != mLastTouchX || y != mLastTouchY) {
mLastTouchX = x;
mLastTouchY = y;
moved = true;
}
ensureProperPupilOffsets();
mIsPupilChanged = mIsPupilChanged || moved;
}
else {
if (mActivePointerId2 == INVALID_POINTER_ID) {
// Only move if the ScaleGestureDetector isn't processing a gesture.
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mOverlayX += dx / mScaleFactor / mBitmap.getWidth();
mOverlayY += dy / mScaleFactor / mBitmap.getHeight();
}
else {
// When resizing, move according to the center of the two pinch points
final int pointerIndex2 = ev.findPointerIndex(mActivePointerId2);
final float x0 = (ev.getX(pointerIndex2) + x) / 2;
final float y0 = (ev.getY(pointerIndex2) + y) / 2;
final float dx = x0 - mLastTouchX0;
final float dy = y0 - mLastTouchY0;
mOverlayX += dx / mScaleFactor / mBitmap.getWidth();
mOverlayY += dy / mScaleFactor / mBitmap.getHeight();
if (mOverlayScaleFactor != mLastOverlayScaleFactor) {
// When resizing, then position also changes
final float changeFactor = mOverlayScaleFactor / mLastOverlayScaleFactor;
final float pinchX = (x0 - getWidth() / 2) / mScaleFactor / mBitmap.getWidth() + mPosX;
final float pinchY = (y0 - getHeight() / 2) / mScaleFactor / mBitmap.getHeight() + mPosY;
mOverlayX = pinchX + (mOverlayX - pinchX) * changeFactor;
mOverlayY = pinchY + (mOverlayY - pinchY) * changeFactor;
mLastOverlayScaleFactor = mOverlayScaleFactor;
moved = true;
}
mLastTouchX0 = x0;
mLastTouchY0 = y0;
}
if (x != mLastTouchX || y != mLastTouchY) {
mLastTouchX = x;
mLastTouchY = y;
moved = true;
}
if (mOverlayX < 0) {
mOverlayX = 0;
}
if (mOverlayY < 0) {
mOverlayY = 0;
}
if (mOverlayX > 1) {
mOverlayX = 1f;
}
if (mOverlayY > 1) {
mOverlayY = 1f;
}
}
refresh(mFullResolutionFlag ? FULL : LOW);
return moved;
}
@Override
protected final void startPointerMove(final MotionEvent ev) {
showNormalResolution();
}
@Override
protected final void finishPointerMove(final MotionEvent ev) {
refresh();
}
/**
* Retrieve the metadata of the image.
*
* @return the metadata of the image
*/
@Nullable
public final JpegMetadata getMetadata() {
return mMetadata;
}
/**
* Create a bitmap containing the current view in full resolution (incl. brightness/contrast).
*
* @return The bitmap in full resolution.
*/
private Bitmap createFullResolutionBitmap() {
if (mBitmap == null) {
return null;
}
float leftX = mPosX * mBitmap.getWidth() - (float) getWidth() / 2 / mScaleFactor;
float rightX = mPosX * mBitmap.getWidth() + (float) getWidth() / 2 / mScaleFactor;
float upperY = mPosY * mBitmap.getHeight() - (float) getHeight() / 2 / mScaleFactor;
float lowerY = mPosY * mBitmap.getHeight() + (float) getHeight() / 2 / mScaleFactor;
// The image part which needs to be displayed
float minX = Math.max(0, leftX / mBitmap.getWidth());
float maxX = Math.min(1, rightX / mBitmap.getWidth());
float minY = Math.max(0, upperY / mBitmap.getHeight());
float maxY = Math.min(1, lowerY / mBitmap.getHeight());
if (maxX <= minX || maxY <= minY) {
// Image is outside of the view
return null;
}
// The distance of the displayed image from the view borders.
int offsetX = Math.round(-Math.min(0, leftX) * mScaleFactor);
int offsetY = Math.round(-Math.min(0, upperY) * mScaleFactor);
int offsetMaxX = Math.round(Math.max(rightX - mBitmap.getWidth(), 0) * mScaleFactor);
int offsetMaxY = Math.round(Math.max(lowerY - mBitmap.getHeight(), 0) * mScaleFactor);
try {
Bitmap bitmapFull = mBitmapFull;
if (bitmapFull == null) {
bitmapFull = mEyePhoto.getFullBitmap();
if (mFullResolutionFlag) {
mBitmapFull = bitmapFull;
if (mRetainFragment != null) {
mRetainFragment.setBitmapFullResolution(bitmapFull);
}
}
}
Bitmap partialBitmap =
ImageUtil.getPartialBitmap(bitmapFull, minX, maxX, minY, maxY);
Bitmap scaledPartialBitmap =
Bitmap.createScaledBitmap(partialBitmap, getWidth() - offsetMaxX - offsetX, getHeight()
- offsetMaxY
- offsetY, false);
Bitmap bitmapFullResolution = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmapFullResolution);
canvas.drawBitmap(scaledPartialBitmap, offsetX, offsetY, null);
return bitmapFullResolution;
}
catch (Exception e) {
// NullPointerExceptions might occur in parallel scenarios.
return null;
}
}
/**
* Tell the view if it should automatically display in full resolution.
*
* @param fullResolutionFlag if true, view shows automatically in full resolution.
*/
public final void allowFullResolution(final boolean fullResolutionFlag) {
mFullResolutionFlag = fullResolutionFlag;
}
/**
* Add the current overlay to the partial Bitmap. (This is done similar to refresh().)
*
* @param partialBitmap the partial bitmap before applying the overlay
* @return the partial bitmap with overlay.
*/
public final Bitmap addOverlayToPartialBitmap(@NonNull final Bitmap partialBitmap) {
List<Integer> overlayPositions = getOverlayPositions();
if (overlayPositions.size() == 0) {
return partialBitmap;
}
Drawable[] layers = new Drawable[overlayPositions.size() + 1];
layers[0] = new BitmapDrawable(getResources(), partialBitmap);
for (int i = 0; i < overlayPositions.size(); i++) {
layers[i + 1] = getOverlayDrawable(overlayPositions.get(i));
}
LayerDrawable layerDrawable = new LayerDrawable(layers);
// position overlays
for (int i = 1; i < layerDrawable.getNumberOfLayers(); i++) {
boolean isPupil = overlayPositions.get(i - 1) == OVERLAY_PUPIL_INDEX;
if (isPupil) {
float totalPupilOverlayScaleFactor = mPupilOverlayScaleFactor * mOverlayScaleFactor * OVERLAY_SIZE / 2;
float pupilAdjustedOverlayX =
mOverlayX + mPupilOverlayX * mOverlayScaleFactor * OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO / mBitmap.getWidth();
float pupilAdjustedOverlayY =
mOverlayY + mPupilOverlayY * mOverlayScaleFactor * OVERLAY_SIZE * OVERLAY_CIRCLE_RATIO / mBitmap.getHeight();
layerDrawable.setLayerInset(i,
(int) (((pupilAdjustedOverlayX - mPosX) * mBitmap.getWidth() - totalPupilOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getWidth() / 2),
(int) (((pupilAdjustedOverlayY - mPosY) * mBitmap.getHeight() - totalPupilOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getHeight() / 2),
(int) (((mPosX - pupilAdjustedOverlayX) * mBitmap.getWidth() - totalPupilOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getWidth() / 2),
(int) (((mPosY - pupilAdjustedOverlayY) * mBitmap.getHeight() - totalPupilOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getHeight() / 2));
}
else {
layerDrawable.setLayerInset(i,
(int) (((mOverlayX - mPosX) * mBitmap.getWidth() - OVERLAY_SIZE / 2 * mOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getWidth() / 2),
(int) (((mOverlayY - mPosY) * mBitmap.getHeight() - OVERLAY_SIZE / 2 * mOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getHeight() / 2),
(int) (((mPosX - mOverlayX) * mBitmap.getWidth() - OVERLAY_SIZE / 2 * mOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getWidth() / 2),
(int) (((mPosY - mOverlayY) * mBitmap.getHeight() - OVERLAY_SIZE / 2 * mOverlayScaleFactor) * mScaleFactor
+ (float) partialBitmap.getHeight() / 2));
}
}
layerDrawable.setBounds(0, 0, partialBitmap.getWidth(), partialBitmap.getHeight());
Bitmap canvasBitmap = Bitmap.createBitmap(partialBitmap.getWidth(), partialBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(canvasBitmap);
layerDrawable.draw(canvas);
return canvasBitmap;
}
/**
* Show the current view in full resolution.
*
* @param async A flag indicating if the bitmap creation should happen in a separate thread.
*/
public final void showFullResolutionSnapshot(final boolean async) {
if (async && !mFullResolutionFlag) {
// Do not trigger full resolution thread if flag is configured for manual handling of full resolution.
return;
}
Thread fullResolutionThread = new Thread() {
@Override
public void run() {
final Bitmap partialBitmapWithOverlay = getFullResolutionBitmapWithOverlay();
if (isInterrupted()) {
// Do not display the result if the thread has been interrupted.
cleanFullResolutionBitmaps(false);
}
else {
// Make a straight display of this bitmap without any matrix transformation.
// Will be reset by regular view as soon as the screen is touched again.
post(new Runnable() {
@Override
public void run() {
if (mPartialBitmapFullResolution != null) {
setImageBitmap(partialBitmapWithOverlay);
mShowingFullResolution = true;
setMatrix();
}
}
});
}
if (async) {
// start next thread in queue
synchronized (mFullResolutionThreads) {
mFullResolutionThreads.remove(Thread.currentThread());
if (mFullResolutionThreads.size() > 0) {
mFullResolutionThreads.get(0).start();
}
}
}
}
};
if (async) {
synchronized (mFullResolutionThreads) {
if (mFullResolutionThreads.size() > 1) {
// at most two threads in list
mFullResolutionThreads.remove(1);
}
mFullResolutionThreads.add(fullResolutionThread);
if (mFullResolutionThreads.size() == 1) {
// only start if no thread is running
fullResolutionThread.start();
}
}
}
else {
try {
fullResolutionThread.start();
fullResolutionThread.join();
}
catch (InterruptedException e) {
// do nothing
}
}
}
/**
* Create the full resolution bitmap including the overlay.
*
* @return The bitmap.
*/
private Bitmap getFullResolutionBitmapWithOverlay() {
if (mPartialBitmapFullResolution == null) {
try {
mPartialBitmapFullResolution = createFullResolutionBitmap();
}
catch (OutOfMemoryError e) {
Log.e(Application.TAG, "Out of memory while creating full resolution bitmap", e);
mPartialBitmapFullResolution = null;
}
if (mPartialBitmapFullResolution == null) {
return null;
}
}
if (mPartialBitmapFullResolutionWithBrightness == null) {
try {
mPartialBitmapFullResolutionWithBrightness =
ImageUtil.changeBitmapColors(mPartialBitmapFullResolution, mContrast, mBrightness, mSaturation, mColorTemperature);
}
catch (OutOfMemoryError e) {
Log.e(Application.TAG, "Out of memory while creating full resolution bitmap with brightness", e);
mPartialBitmapFullResolutionWithBrightness = null;
}
if (mPartialBitmapFullResolutionWithBrightness == null) {
return null;
}
}
return addOverlayToPartialBitmap(mPartialBitmapFullResolutionWithBrightness);
}
/**
* Clean the cached full resolution bitmaps. In case of full cleaning, a normal resolution snapshot is displayed.
*
* @param onlyBrightness Flag indicating if only the brightness/contrast bitmap is cleaned, but the position is kept.
*/
private void cleanFullResolutionBitmaps(final boolean onlyBrightness) {
mPartialBitmapFullResolutionWithBrightness = null;
if (!onlyBrightness) {
mPartialBitmapFullResolution = null;
if (mShowingFullResolution) {
setImageBitmap(mCanvasBitmap);
mShowingFullResolution = false;
setMatrix();
}
}
}
/**
* Interrupt the full resolution snapshot creation, if in process.
*/
private void interruptFullResolutionThread() {
synchronized (mFullResolutionThreads) {
if (mFullResolutionThreads.size() > 0) {
mFullResolutionThreads.get(0).interrupt();
if (mFullResolutionThreads.size() > 1) {
mFullResolutionThreads.remove(1);
}
}
}
cleanFullResolutionBitmaps(false);
}
/**
* Show normal resolution again after having the full resolution snapshot.
*/
public final void showNormalResolution() {
if (mNeedsBitmapRefresh) {
refresh(HIGH);
}
else {
interruptFullResolutionThread();
}
}
/**
* Get the URL of the image for sharing.
*
* @param currentView If true, the current view will be considered, otherwise the full bitmap.
* @param tempFileName The name of the temporary file (required in case of currentView=true)
* @return The URL.
*/
public Uri getBitmapUri(final boolean currentView, final String tempFileName) {
if (currentView) {
return ImageUtil.getUriForFullResolutionBitmap(getFullResolutionBitmapWithOverlay(), tempFileName);
}
else {
return MediaStoreUtil.getUriFromFile(getEyePhoto().getAbsolutePath());
}
}
@Override
protected final void setMatrix() {
if (mShowingFullResolution) {
setImageMatrix(null);
}
else {
super.setMatrix();
}
invalidate();
}
/**
* Override requestLayout to show normal resolution.
*/
@Override
public final void requestLayout() {
if (mBitmap != null && (getWidth() != mLastWidth || getHeight() != mLastHeight)) {
// if view size changed, then calculate full resolution image again
showNormalResolution();
showFullResolutionSnapshot(true);
}
super.requestLayout();
mLastHeight = getHeight();
mLastWidth = getWidth();
}
/**
* Store the comment in the image.
*
* @param comment the comment to be stored.
*/
public final void storeComment(final String comment) {
if (mInitialized && mMetadata != null) {
mMetadata.setComment(comment);
mEyePhoto.storeImageMetadata(mMetadata);
PreferenceUtil.incrementCounter(R.string.key_statistics_countcomment);
TrackingUtil.sendEvent(Category.EVENT_USER, "Edit comment", null);
}
}
/**
* Remove cached full bitmap from memory.
*/
public final void cleanFullBitmap() {
mBitmapFull = null;
if (mRetainFragment != null) {
mRetainFragment.setBitmapFullResolution(null);
}
}
/*
* Save brightness, contrast and overlay position.
*/
@NonNull
@Override
protected final Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putFloat("mOverlayX", this.mOverlayX);
bundle.putFloat("mOverlayY", this.mOverlayY);
bundle.putFloat("mOverlayScaleFactor", this.mOverlayScaleFactor);
bundle.putFloat("mPupilOverlayX", this.mPupilOverlayX);
bundle.putFloat("mPupilOverlayY", this.mPupilOverlayY);
bundle.putFloat("mPupilOverlayScaleFactor", this.mPupilOverlayScaleFactor);
bundle.putBooleanArray("mShowOverlay", this.mShowOverlay);
bundle.putBoolean("mLocked", this.mLocked);
bundle.putSerializable("mPinchMode", mPinchMode);
bundle.putFloat("mBrightness", this.mBrightness);
bundle.putFloat("mContrast", this.mContrast);
bundle.putFloat("mSaturation", this.mSaturation);
bundle.putFloat("mColorTemperature", this.mColorTemperature);
bundle.putInt("mOverlayColor", mOverlayColor);
bundle.putParcelable("mMetadata", mMetadata);
return bundle;
}
@Override
protected final void onRestoreInstanceState(final Parcelable state) {
Parcelable enhancedState = state;
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
this.mOverlayX = bundle.getFloat("mOverlayX");
this.mOverlayY = bundle.getFloat("mOverlayY");
this.mOverlayScaleFactor = bundle.getFloat("mOverlayScaleFactor");
mLastOverlayScaleFactor = mOverlayScaleFactor;
this.mPupilOverlayX = bundle.getFloat("mPupilOverlayX");
this.mPupilOverlayY = bundle.getFloat("mPupilOverlayY");
this.mPupilOverlayScaleFactor = bundle.getFloat("mPupilOverlayScaleFactor");
mLastPupilOverlayScaleFactor = mPupilOverlayScaleFactor;
this.mShowOverlay = bundle.getBooleanArray("mShowOverlay");
this.mLocked = bundle.getBoolean("mLocked");
this.mPinchMode = (PinchMode) bundle.getSerializable("mPinchMode");
this.mBrightness = bundle.getFloat("mBrightness");
this.mContrast = bundle.getFloat("mContrast");
this.mSaturation = bundle.getFloat("mSaturation");
this.mColorTemperature = bundle.getFloat("mColorTemperature");
this.mOverlayColor = bundle.getInt("mOverlayColor");
this.mMetadata = bundle.getParcelable("mMetadata");
enhancedState = bundle.getParcelable("instanceState");
}
super.onRestoreInstanceState(enhancedState);
}
/**
* Set the reference that allows GUI updates.
*
* @param updater The GUI Element updater
*/
public final void setGuiElementUpdater(final GuiElementUpdater updater) {
mGuiElementUpdater = updater;
}
/**
* A listener determining the scale factor.
*/
private class OverlayScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(@NonNull final ScaleGestureDetector detector) {
mOverlayScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mOverlayScaleFactor =
Math.max(MIN_OVERLAY_SCALE_FACTOR, Math.min(mOverlayScaleFactor, MAX_OVERLAY_SCALE_FACTOR));
invalidate();
return true;
}
}
/**
* A listener determining the pupil scale factor.
*/
private class PupilOverlayScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(@NonNull final ScaleGestureDetector detector) {
mPupilOverlayScaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
mPupilOverlayScaleFactor =
Math.max(MIN_PUPIL_SCALE_FACTOR, Math.min(mPupilOverlayScaleFactor, MAX_PUPIL_SCALE_FACTOR));
invalidate();
return true;
}
}
/**
* Interface that allows the view to update GUI elements from the activity holding the view.
*/
public interface GuiElementUpdater {
/**
* Set the checked status of the lock button.
*
* @param checked the lock status.
*/
void setLockChecked(boolean checked);
/**
* Update the brightness bar.
*
* @param brightness The brightness.
*/
void updateSeekbarBrightness(float brightness);
/**
* Update the contrast bar.
*
* @param contrast The contrast.
*/
void updateSeekbarContrast(float contrast);
/**
* Update the saturation bar.
*
* @param saturation The saturation.
*/
void updateSeekbarSaturation(float saturation);
/**
* Update the color temperature bar.
*
* @param colorTemperature The color temperature.
*/
void updateSeekbarColorTemperature(float colorTemperature);
/**
* Update the overlay color button.
*
* @param color The color displayed in the button.
*/
void updateOverlayColorButton(int color);
/**
* Retrieve the default color for the overlay.
*
* @return The default color for the overlay.
*/
int getOverlayDefaultColor();
/**
* Reset the overlays.
*/
void resetOverlays();
}
/**
* Helper listFoldersFragment to retain the bitmap on configuration change.
*/
public static class RetainFragment extends PinchImageView.RetainFragment {
/**
* Tag to be used as identifier of the fragment.
*/
private static final String TAG = "RetainFragment";
/**
* The small version of the bitmap.
*/
private Bitmap mRetainbitmapSmall;
private Bitmap getBitmapSmall() {
return mRetainbitmapSmall;
}
private void setBitmapSmall(final Bitmap bitmapSmall) {
this.mRetainbitmapSmall = bitmapSmall;
}
/**
* The full resolution bitmap.
*/
private Bitmap mRetainBitmapFullResolution;
private Bitmap getBitmapFullResolution() {
return mRetainBitmapFullResolution;
}
private void setBitmapFullResolution(final Bitmap bitmapFullResolution) {
this.mRetainBitmapFullResolution = bitmapFullResolution;
}
/**
* Get the retainFragment - search it by the index. If not found, create a new one.
*
* @param fm The fragment manager handling this fragment.
* @param index The index of the view (required in case of multiple PinchImageViews to be retained).
* @return the retainFragment.
*/
@NonNull
public static RetainFragment findOrCreateRetainFragment(@NonNull final FragmentManager fm, final int index) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG + index);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG + index).commit();
}
return fragment;
}
}
/**
* Enumeration giving the resolution with which the picture is displayed.
*/
public enum Resolution {
/**
* Thumbnail resolution.
*/
LOW,
/**
* High resolution, as specified in the settings.
*/
HIGH,
/**
* Full resolution.
*/
FULL,
/**
* Full resolution, but high resolution should be prepared.
*/
FULL_HIGH
}
/**
* The way of pinching.
*/
public enum PinchMode {
/**
* Pinch everything together.
*/
ALL,
/**
* Pinch only the overlay.
*/
OVERLAY,
/**
* Pinch the pupil overlay.
*/
PUPIL,
/**
* Pinch the pupil overlay, but keep it in the center.
*/
PUPIL_CENTER
}
}