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 } }