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;
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;
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);
}
@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);
}
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();
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
*/
package com.android.camera.widget;
+import com.google.common.base.Optional;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
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;
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<Listener> 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) {
mVisibleAnimator.end();
setVisibility(View.VISIBLE);
mVisibleAnimator.start();
+ if (mListener.isPresent()) {
+ mListener.get().onBeginToShowModeOptions();
+ }
}
mIsHiddenOrHiding = false;
}
mVisibleAnimator.cancel();
mHiddenAnimator.end();
mHiddenAnimator.start();
+ if (mListener.isPresent()) {
+ mListener.get().onBeginToHideModeOptions();
+ }
}
mIsHiddenOrHiding = true;
}
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
}
/**
+ * 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) {
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);
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;
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;
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.
centerX,
centerY,
thumbnailPaint);
-
}
// Draw the reveal while circle.
}
/**
- * 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;
}
/**
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 =