From 54ac03ba6ae3e739df74c6d9e35fda9017be07d7 Mon Sep 17 00:00:00 2001 From: Senpo Hu Date: Mon, 2 Feb 2015 12:41:57 -0800 Subject: [PATCH] Adjust capture indicator position while open/close mode options. This CL also fixes the layout issue in landscape mode when mode option indicators are visible. Bug: 18866551 Bug: 18317565 Bug: 18905659 Change-Id: I34ca5aa77aa9d37a7f0ad3d2d7efbed92fe94f2b --- .../camera/ui/StickyBottomCaptureLayout.java | 156 +++++++++++++++++++-- .../camera/ui/motion/InterpolatorHelper.java | 45 ++++++ src/com/android/camera/widget/ModeOptions.java | 49 ++++++- .../android/camera/widget/ModeOptionsOverlay.java | 28 +++- .../camera/widget/RoundedThumbnailView.java | 65 +++------ 5 files changed, 279 insertions(+), 64 deletions(-) create mode 100644 src/com/android/camera/ui/motion/InterpolatorHelper.java diff --git a/src/com/android/camera/ui/StickyBottomCaptureLayout.java b/src/com/android/camera/ui/StickyBottomCaptureLayout.java index 2c0dec074..14c7e7368 100644 --- a/src/com/android/camera/ui/StickyBottomCaptureLayout.java +++ b/src/com/android/camera/ui/StickyBottomCaptureLayout.java @@ -17,6 +17,9 @@ package com.android.camera.ui; import android.content.Context; +import android.content.res.Configuration; +import android.view.animation.Interpolator; +import android.graphics.PointF; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; @@ -24,6 +27,9 @@ import android.widget.FrameLayout; import com.android.camera.CaptureLayoutHelper; import com.android.camera.debug.Log; +import com.android.camera.ui.motion.InterpolatorHelper; +import com.android.camera.widget.ModeOptions; +import com.android.camera.widget.ModeOptionsOverlay; import com.android.camera.widget.RoundedThumbnailView; import com.android.camera2.R; @@ -37,10 +43,40 @@ public class StickyBottomCaptureLayout extends FrameLayout { private final static Log.Tag TAG = new Log.Tag("StickyBotCapLayout"); private RoundedThumbnailView mRoundedThumbnailView; - private View mModeOptionsOverlay; + private ModeOptionsOverlay mModeOptionsOverlay; private View mBottomBar; private CaptureLayoutHelper mCaptureLayoutHelper = null; + private ModeOptions.Listener mModeOptionsListener = new ModeOptions.Listener() { + @Override + public void onBeginToShowModeOptions() { + final PointF thumbnailViewPosition = getRoundedThumbnailPosition( + mCaptureLayoutHelper.getUncoveredPreviewRect(), + false, + mModeOptionsOverlay.getModeOptionsToggleWidth()); + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + animateCaptureIndicatorToY(thumbnailViewPosition.y); + } else { + animateCaptureIndicatorToX(thumbnailViewPosition.x); + } + } + + @Override + public void onBeginToHideModeOptions() { + final PointF thumbnailViewPosition = getRoundedThumbnailPosition( + mCaptureLayoutHelper.getUncoveredPreviewRect(), + true, + mModeOptionsOverlay.getModeOptionsToggleWidth()); + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + animateCaptureIndicatorToY(thumbnailViewPosition.y); + } else { + animateCaptureIndicatorToX(thumbnailViewPosition.x); + } + } + }; + public StickyBottomCaptureLayout(Context context, AttributeSet attrs) { super(context, attrs); } @@ -48,7 +84,8 @@ public class StickyBottomCaptureLayout extends FrameLayout { @Override public void onFinishInflate() { mRoundedThumbnailView = (RoundedThumbnailView) findViewById(R.id.rounded_thumbnail_view); - mModeOptionsOverlay = findViewById(R.id.mode_options_overlay); + mModeOptionsOverlay = (ModeOptionsOverlay) findViewById(R.id.mode_options_overlay); + mModeOptionsOverlay.setModeOptionsListener(mModeOptionsListener); mBottomBar = findViewById(R.id.bottom_bar); } @@ -65,19 +102,114 @@ public class StickyBottomCaptureLayout extends FrameLayout { Log.e(TAG, "Capture layout helper needs to be set first."); return; } - RectF drawRect = new RectF(left, top, right, bottom); + // Layout mode options overlay. RectF uncoveredPreviewRect = mCaptureLayoutHelper.getUncoveredPreviewRect(); - RectF bottomBarRect = mCaptureLayoutHelper.getBottomBarRect(); - RectF roundedThumbnailViewRect = - mRoundedThumbnailView.getDesiredLayout(drawRect, uncoveredPreviewRect); - mRoundedThumbnailView.layout( - (int) roundedThumbnailViewRect.left, - (int) roundedThumbnailViewRect.top, - (int) roundedThumbnailViewRect.right, - (int) roundedThumbnailViewRect.bottom); mModeOptionsOverlay.layout((int) uncoveredPreviewRect.left, (int) uncoveredPreviewRect.top, (int) uncoveredPreviewRect.right, (int) uncoveredPreviewRect.bottom); + + // Layout capture indicator. + PointF roundedThumbnailViewPosition = getRoundedThumbnailPosition( + uncoveredPreviewRect, + mModeOptionsOverlay.isModeOptionsHidden(), + mModeOptionsOverlay.getModeOptionsToggleWidth()); + mRoundedThumbnailView.layout( + (int) roundedThumbnailViewPosition.x, + (int) roundedThumbnailViewPosition.y, + (int) roundedThumbnailViewPosition.x + mRoundedThumbnailView.getMeasuredWidth(), + (int) roundedThumbnailViewPosition.y + mRoundedThumbnailView.getMeasuredHeight()); + + // Layout bottom bar. + RectF bottomBarRect = mCaptureLayoutHelper.getBottomBarRect(); mBottomBar.layout((int) bottomBarRect.left, (int) bottomBarRect.top, (int) bottomBarRect.right, (int) bottomBarRect.bottom); } -} \ No newline at end of file + + /** + * Calculates the desired layout of capture indicator. + * + * @param uncoveredPreviewRect The uncovered preview bound which contains mode option + * overlay and capture indicator. + * @param isModeOptionsHidden Whether the mode options button are hidden. + * @param modeOptionsToggleWidth The width of mode options toggle (three dots button). + * @return the desired view bound for capture indicator. + */ + private PointF getRoundedThumbnailPosition( + RectF uncoveredPreviewRect, boolean isModeOptionsHidden, float modeOptionsToggleWidth) { + final float threeDotsButtonDiameter = + getResources().getDimension(R.dimen.option_button_circle_size); + final float threeDotsButtonPadding = + getResources().getDimension(R.dimen.mode_options_toggle_padding); + final float modeOptionsHeight = getResources().getDimension(R.dimen.mode_options_height); + + final float roundedThumbnailViewSize = mRoundedThumbnailView.getMeasuredWidth(); + final float roundedThumbnailFinalSize = mRoundedThumbnailView.getThumbnailFinalDiameter(); + final float roundedThumbnailViewPadding = mRoundedThumbnailView.getThumbnailPadding(); + + // The view bound is based on the maximal ripple ring diameter. This is the diff of maximal + // ripple ring radius and the final thumbnail radius. + final float radiusDiffBetweenViewAndThumbnail = + (roundedThumbnailViewSize - roundedThumbnailFinalSize) / 2.0f; + final float distanceFromModeOptions = roundedThumbnailViewPadding + + roundedThumbnailFinalSize + radiusDiffBetweenViewAndThumbnail; + + final int orientation = getResources().getConfiguration().orientation; + + float x = 0; + float y = 0; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + // The view finder of 16:9 aspect ratio might have a black padding. + x = uncoveredPreviewRect.right - distanceFromModeOptions; + + y = uncoveredPreviewRect.bottom; + if (isModeOptionsHidden) { + y -= threeDotsButtonPadding + threeDotsButtonDiameter; + } else { + y -= modeOptionsHeight; + } + y -= distanceFromModeOptions; + } + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + if (isModeOptionsHidden) { + x = uncoveredPreviewRect.right - threeDotsButtonPadding - modeOptionsToggleWidth; + } else { + x = uncoveredPreviewRect.right - modeOptionsHeight; + } + x -= distanceFromModeOptions; + y = uncoveredPreviewRect.top + roundedThumbnailViewPadding - + radiusDiffBetweenViewAndThumbnail; + } + return new PointF(x, y); + } + + private void animateCaptureIndicatorToX(float x) { + final Interpolator interpolator = + InterpolatorHelper.getLinearOutSlowInInterpolator(getContext()); + mRoundedThumbnailView.animate() + .setDuration(ModeOptions.PADDING_ANIMATION_TIME) + .setInterpolator(interpolator) + .x(x) + .withEndAction(new Runnable() { + @Override + public void run() { + mRoundedThumbnailView.setTranslationX(0.0f); + requestLayout(); + } + }); + } + + private void animateCaptureIndicatorToY(float y) { + final Interpolator interpolator = + InterpolatorHelper.getLinearOutSlowInInterpolator(getContext()); + mRoundedThumbnailView.animate() + .setDuration(ModeOptions.PADDING_ANIMATION_TIME) + .setInterpolator(interpolator) + .y(y) + .withEndAction(new Runnable() { + @Override + public void run() { + mRoundedThumbnailView.setTranslationY(0.0f); + requestLayout(); + } + }); + } +} diff --git a/src/com/android/camera/ui/motion/InterpolatorHelper.java b/src/com/android/camera/ui/motion/InterpolatorHelper.java new file mode 100644 index 000000000..b78bb8ba9 --- /dev/null +++ b/src/com/android/camera/ui/motion/InterpolatorHelper.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui.motion; + +import android.content.Context; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.camera.util.ApiHelper; + +import javax.annotation.Nonnull; + +public class InterpolatorHelper { + private static Interpolator LINEAR_OUT_SLOW_IN = null; + + @Nonnull + public static Interpolator getLinearOutSlowInInterpolator(final @Nonnull Context context) { + if (LINEAR_OUT_SLOW_IN != null) { + return LINEAR_OUT_SLOW_IN; + } + + if (ApiHelper.isLOrHigher()) { + LINEAR_OUT_SLOW_IN = AnimationUtils.loadInterpolator( + context, android.R.interpolator.linear_out_slow_in); + } else { + LINEAR_OUT_SLOW_IN = new DecelerateInterpolator(); + } + return LINEAR_OUT_SLOW_IN; + } +} diff --git a/src/com/android/camera/widget/ModeOptions.java b/src/com/android/camera/widget/ModeOptions.java index 118359442..647de6bb7 100644 --- a/src/com/android/camera/widget/ModeOptions.java +++ b/src/com/android/camera/widget/ModeOptions.java @@ -15,6 +15,8 @@ */ package com.android.camera.widget; +import com.google.common.base.Optional; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -55,7 +57,7 @@ public class ModeOptions extends FrameLayout { private static final int RADIUS_ANIMATION_TIME = 250; private static final int SHOW_ALPHA_ANIMATION_TIME = 350; private static final int HIDE_ALPHA_ANIMATION_TIME = 200; - private static final int PADDING_ANIMATION_TIME = 350; + public static final int PADDING_ANIMATION_TIME = 350; private ViewGroup mMainBar; private ViewGroup mActiveBar; @@ -66,8 +68,47 @@ public class ModeOptions extends FrameLayout { private boolean mIsPortrait; private float mRadius = 0f; + /** + * A class implementing this interface will receive callback events from + * mode options. + */ + public interface Listener { + /** + * Called when about to start animating the mode options from hidden + * to visible. + */ + public void onBeginToShowModeOptions(); + + /** + * Called when about to start animating the mode options from visible + * to hidden. + */ + public void onBeginToHideModeOptions(); + } + + /** The listener. */ + private Optional mListener; + public ModeOptions(Context context, AttributeSet attrs) { super(context, attrs); + mListener = Optional.absent(); + } + + /** + * Whether the mode options is hidden or in the middle of fading + * out. + */ + public boolean isHiddenOrHiding() { + return mIsHiddenOrHiding; + } + + /** + * Sets the listener. + * + * @param listener The listener to be set. + */ + public void setListener(Listener listener) { + mListener = Optional.of(listener); } public void setViewToShowHide(View v) { @@ -346,6 +387,9 @@ public class ModeOptions extends FrameLayout { mVisibleAnimator.end(); setVisibility(View.VISIBLE); mVisibleAnimator.start(); + if (mListener.isPresent()) { + mListener.get().onBeginToShowModeOptions(); + } } mIsHiddenOrHiding = false; } @@ -355,6 +399,9 @@ public class ModeOptions extends FrameLayout { mVisibleAnimator.cancel(); mHiddenAnimator.end(); mHiddenAnimator.start(); + if (mListener.isPresent()) { + mListener.get().onBeginToHideModeOptions(); + } } mIsHiddenOrHiding = true; } diff --git a/src/com/android/camera/widget/ModeOptionsOverlay.java b/src/com/android/camera/widget/ModeOptionsOverlay.java index a4a443495..c38360d43 100644 --- a/src/com/android/camera/widget/ModeOptionsOverlay.java +++ b/src/com/android/camera/widget/ModeOptionsOverlay.java @@ -46,8 +46,8 @@ public class ModeOptionsOverlay extends FrameLayout private final static Log.Tag TAG = new Log.Tag("ModeOptionsOverlay"); private static final int BOTTOMBAR_OPTIONS_TIMEOUT_MS = 2000; - private final static int BOTTOM_RIGHT = Gravity.BOTTOM | Gravity.RIGHT; - private final static int TOP_RIGHT = Gravity.TOP | Gravity.RIGHT; + private static final int BOTTOM_RIGHT = Gravity.BOTTOM | Gravity.RIGHT; + private static final int TOP_RIGHT = Gravity.TOP | Gravity.RIGHT; private ModeOptions mModeOptions; // need a reference to set the onClickListener and fix the layout gravity on orientation change @@ -61,6 +61,21 @@ public class ModeOptionsOverlay extends FrameLayout } /** + * Whether the mode options are hidden. + */ + public boolean isModeOptionsHidden() { + return mModeOptions.isHiddenOrHiding(); + } + + /** + * Gets the current width of the mode options toggle including the three dots and various mode + * option indicators. + */ + public float getModeOptionsToggleWidth() { + return mModeOptionsToggle.getWidth(); + } + + /** * Sets a capture layout helper to query layout rect from. */ public void setCaptureLayoutHelper(CaptureLayoutHelper helper) { @@ -71,6 +86,15 @@ public class ModeOptionsOverlay extends FrameLayout mModeOptionsToggle.setClickable(clickable); } + /** + * Sets the mode options listener. + * + * @param listener The listener to be set. + */ + public void setModeOptionsListener(ModeOptions.Listener listener) { + mModeOptions.setListener(listener); + } + @Override public void onFinishInflate() { mModeOptions = (ModeOptions) findViewById(R.id.mode_options); diff --git a/src/com/android/camera/widget/RoundedThumbnailView.java b/src/com/android/camera/widget/RoundedThumbnailView.java index d6a02f875..ca5ffe62c 100644 --- a/src/com/android/camera/widget/RoundedThumbnailView.java +++ b/src/com/android/camera/widget/RoundedThumbnailView.java @@ -23,7 +23,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; -import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; @@ -36,13 +35,12 @@ import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.android.camera.async.MainThread; import com.android.camera.debug.Log; +import com.android.camera.ui.motion.InterpolatorHelper; import com.android.camera.util.ApiHelper; -import com.android.camera.util.CameraUtil; import com.android.camera2.R; import com.google.common.base.Optional; @@ -226,7 +224,6 @@ public class RoundedThumbnailView extends View { setClickable(true); setOnClickListener(mOnClickListener); - // TODO: Adjust layout when mode option overlay is visible. mThumbnailPadding = getResources().getDimension(R.dimen.rounded_thumbnail_padding); // Load thumbnail pop-out effect constants. @@ -331,7 +328,6 @@ public class RoundedThumbnailView extends View { centerX, centerY, thumbnailPaint); - } // Draw the reveal while circle. @@ -370,44 +366,21 @@ public class RoundedThumbnailView extends View { } /** - * Calculates the desired layout of capture indicator. + * Gets the padding size with mode options and preview edges. * - * @param parentRect The bound of the view which contains capture indicator. - * @param uncoveredPreviewRect The uncovered preview bound which contains mode option - * overlay and capture indicator. - * @return the desired view bound for capture indicator. + * @return The padding size with mode options and preview edges. */ - public RectF getDesiredLayout(RectF parentRect, RectF uncoveredPreviewRect) { - float parentViewWidth = parentRect.right - parentRect.left; - float x = 0; - float y = 0; - - // The view bound is based on the maximal ripple ring diameter. This is the diff of maximal - // ripple ring radius and the final thumbnail radius. - float radius_diff_max_normal = (mRippleRingDiameterEnd - mThumbnailShrinkDiameterEnd) / 2; - float modeSwitchThreeDotsDiameter = mThumbnailShrinkDiameterEnd; - float modeSwitchThreeDotsBottomPadding = mThumbnailPadding; - - int orientation = getResources().getConfiguration().orientation; - int rotation = CameraUtil.getDisplayRotation(); - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - // The view finder of 16:9 aspect ratio might have a black padding. - float previewRightEdgeGap = - parentRect.right - uncoveredPreviewRect.right; - x = parentViewWidth - previewRightEdgeGap - mThumbnailPadding - - mThumbnailShrinkDiameterEnd - radius_diff_max_normal; - y = uncoveredPreviewRect.bottom; - y -= modeSwitchThreeDotsBottomPadding + modeSwitchThreeDotsDiameter + - mThumbnailPadding + mThumbnailShrinkDiameterEnd + radius_diff_max_normal; - } - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - float previewTopEdgeGap = uncoveredPreviewRect.top; - x = uncoveredPreviewRect.right; - x -= modeSwitchThreeDotsBottomPadding + modeSwitchThreeDotsDiameter + - mThumbnailPadding + mThumbnailShrinkDiameterEnd + radius_diff_max_normal; - y = previewTopEdgeGap + mThumbnailPadding - radius_diff_max_normal; - } - return new RectF(x, y, x + mRippleRingDiameterEnd, y + mRippleRingDiameterEnd); + public float getThumbnailPadding() { + return mThumbnailPadding; + } + + /** + * Gets the diameter of the thumbnail image after the revealing animation. + * + * @return The diameter of the thumbnail image after the revealing animation. + */ + public float getThumbnailFinalDiameter() { + return mThumbnailShrinkDiameterEnd; } /** @@ -559,14 +532,8 @@ public class RoundedThumbnailView extends View { if (mRippleAnimator == null) { // Ripple effect uses linear_out_slow_in interpolator. - Interpolator rippleInterpolator; - if (ApiHelper.isLOrHigher()) { - // Both phases use fast_out_flow_in interpolator. - rippleInterpolator = AnimationUtils.loadInterpolator( - getContext(), android.R.interpolator.linear_out_slow_in); - } else { - rippleInterpolator = new DecelerateInterpolator(); - } + Interpolator rippleInterpolator = + InterpolatorHelper.getLinearOutSlowInInterpolator(getContext()); // When start shrinking the thumbnail, a ripple effect is triggered at the same time. mRippleAnimator = -- 2.11.0