| /* |
| * Copyright (C) 2013 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; |
| |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.RectF; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.LayerDrawable; |
| import android.graphics.drawable.TransitionDrawable; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.widget.FrameLayout; |
| import android.widget.ImageButton; |
| |
| import com.android.camera.CaptureLayoutHelper; |
| import com.android.camera.ShutterButton; |
| import com.android.camera.debug.Log; |
| import com.android.camera.util.ApiHelper; |
| import com.android.camera.util.CameraUtil; |
| import com.android.camera2.R; |
| |
| /** |
| * BottomBar swaps its width and height on rotation. In addition, it also |
| * changes gravity and layout orientation based on the new orientation. |
| * Specifically, in landscape it aligns to the right side of its parent and lays |
| * out its children vertically, whereas in portrait, it stays at the bottom of |
| * the parent and has a horizontal layout orientation. |
| */ |
| public class BottomBar extends FrameLayout { |
| |
| private static final Log.Tag TAG = new Log.Tag("BottomBar"); |
| |
| private static final int CIRCLE_ANIM_DURATION_MS = 300; |
| private static final int DRAWABLE_MAX_LEVEL = 10000; |
| private static final int MODE_CAPTURE = 0; |
| private static final int MODE_INTENT = 1; |
| private static final int MODE_INTENT_REVIEW = 2; |
| private static final int MODE_CANCEL = 3; |
| |
| private int mMode; |
| |
| private final int mBackgroundAlphaOverlay; |
| private final int mBackgroundAlphaDefault; |
| private boolean mOverLayBottomBar; |
| |
| private FrameLayout mCaptureLayout; |
| private FrameLayout mCancelLayout; |
| private TopRightWeightedLayout mIntentReviewLayout; |
| |
| private ShutterButton mShutterButton; |
| private ImageButton mCancelButton; |
| |
| private int mBackgroundColor; |
| private int mBackgroundPressedColor; |
| private int mBackgroundAlpha = 0xff; |
| |
| private boolean mDrawCircle; |
| private final float mCircleRadius; |
| private CaptureLayoutHelper mCaptureLayoutHelper = null; |
| |
| // for Android L, these backgrounds are RippleDrawables (ISA LayerDrawable) |
| // pre-L, they're plain old LayerDrawables |
| private final LayerDrawable[] mShutterButtonBackgrounds; |
| // a reference to the shutter background's first contained drawable |
| // if it's an animated circle drawable (for video mode) |
| private AnimatedCircleDrawable mAnimatedCircleDrawable; |
| // a reference to the shutter background's first contained drawable |
| // if it's a color drawable (for all other modes) |
| private ColorDrawable mColorDrawable; |
| |
| private RectF mRect = new RectF(); |
| |
| public BottomBar(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| mCircleRadius = getResources() |
| .getDimensionPixelSize(R.dimen.video_capture_circle_diameter) / 2; |
| mBackgroundAlphaOverlay = getResources() |
| .getInteger(R.integer.bottom_bar_background_alpha_overlay); |
| mBackgroundAlphaDefault = getResources() |
| .getInteger(R.integer.bottom_bar_background_alpha); |
| |
| // preload all the drawable BGs |
| TypedArray ar = context.getResources() |
| .obtainTypedArray(R.array.shutter_button_backgrounds); |
| int len = ar.length(); |
| |
| mShutterButtonBackgrounds = new LayerDrawable[len]; |
| for (int i = 0; i < len; i++) { |
| int drawableId = ar.getResourceId(i, -1); |
| LayerDrawable shutterBackground = mShutterButtonBackgrounds[i] = |
| (LayerDrawable) context.getResources().getDrawable(drawableId).mutate(); |
| |
| // the background for video has a circle_item drawable placeholder |
| // that gets replaced by an AnimatedCircleDrawable for the cool |
| // shrink-down-to-a-circle effect |
| // all other modes need not do this replace |
| Drawable d = shutterBackground.findDrawableByLayerId(R.id.circle_item); |
| if (d != null) { |
| Drawable animatedCircleDrawable = |
| new AnimatedCircleDrawable((int) mCircleRadius); |
| animatedCircleDrawable.setLevel(DRAWABLE_MAX_LEVEL); |
| shutterBackground |
| .setDrawableByLayerId(R.id.circle_item, animatedCircleDrawable); |
| } |
| } |
| ar.recycle(); |
| } |
| |
| private void setPaintColor(int alpha, int color) { |
| if (mAnimatedCircleDrawable != null) { |
| mAnimatedCircleDrawable.setColor(color); |
| mAnimatedCircleDrawable.setAlpha(alpha); |
| } else if (mColorDrawable != null) { |
| mColorDrawable.setColor(color); |
| mColorDrawable.setAlpha(alpha); |
| } |
| |
| if (mIntentReviewLayout != null) { |
| ColorDrawable intentBackground = (ColorDrawable) mIntentReviewLayout |
| .getBackground(); |
| intentBackground.setColor(color); |
| intentBackground.setAlpha(alpha); |
| } |
| } |
| |
| private void refreshPaintColor() { |
| setPaintColor(mBackgroundAlpha, mBackgroundColor); |
| } |
| |
| private void setCancelBackgroundColor(int alpha, int color) { |
| LayerDrawable layerDrawable = (LayerDrawable) mCancelButton.getBackground(); |
| ColorDrawable colorDrawable = (ColorDrawable) layerDrawable.getDrawable(0); |
| if (!ApiHelper.isLOrHigher()) { |
| colorDrawable.setColor(color); |
| } |
| colorDrawable.setAlpha(alpha); |
| } |
| |
| private void setCaptureButtonUp() { |
| setPaintColor(mBackgroundAlpha, mBackgroundColor); |
| } |
| |
| private void setCaptureButtonDown() { |
| if (!ApiHelper.isLOrHigher()) { |
| setPaintColor(mBackgroundAlpha, mBackgroundPressedColor); |
| } |
| } |
| |
| private void setCancelButtonUp() { |
| setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor); |
| } |
| |
| private void setCancelButtonDown() { |
| setCancelBackgroundColor(mBackgroundAlpha, mBackgroundPressedColor); |
| } |
| |
| @Override |
| public void onFinishInflate() { |
| mCaptureLayout = |
| (FrameLayout) findViewById(R.id.bottombar_capture); |
| mCancelLayout = |
| (FrameLayout) findViewById(R.id.bottombar_cancel); |
| mCancelLayout.setVisibility(View.GONE); |
| |
| mIntentReviewLayout = |
| (TopRightWeightedLayout) findViewById(R.id.bottombar_intent_review); |
| |
| mShutterButton = |
| (ShutterButton) findViewById(R.id.shutter_button); |
| mShutterButton.setOnTouchListener(new OnTouchListener() { |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| if (MotionEvent.ACTION_DOWN == event.getActionMasked()) { |
| setCaptureButtonDown(); |
| } else if (MotionEvent.ACTION_UP == event.getActionMasked() || |
| MotionEvent.ACTION_CANCEL == event.getActionMasked()) { |
| setCaptureButtonUp(); |
| } else if (MotionEvent.ACTION_MOVE == event.getActionMasked()) { |
| mRect.set(0, 0, getWidth(), getHeight()); |
| if (!mRect.contains(event.getX(), event.getY())) { |
| setCaptureButtonUp(); |
| } |
| } |
| return false; |
| } |
| }); |
| |
| mCancelButton = |
| (ImageButton) findViewById(R.id.shutter_cancel_button); |
| mCancelButton.setOnTouchListener(new OnTouchListener() { |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| if (MotionEvent.ACTION_DOWN == event.getActionMasked()) { |
| setCancelButtonDown(); |
| } else if (MotionEvent.ACTION_UP == event.getActionMasked() || |
| MotionEvent.ACTION_CANCEL == event.getActionMasked()) { |
| setCancelButtonUp(); |
| } else if (MotionEvent.ACTION_MOVE == event.getActionMasked()) { |
| mRect.set(0, 0, getWidth(), getHeight()); |
| if (!mRect.contains(event.getX(), event.getY())) { |
| setCancelButtonUp(); |
| } |
| } |
| return false; |
| } |
| }); |
| |
| } |
| |
| /** |
| * Perform a transition from the bottom bar options layout to the bottom bar |
| * capture layout. |
| */ |
| public void transitionToCapture() { |
| mCaptureLayout.setVisibility(View.VISIBLE); |
| mCancelLayout.setVisibility(View.GONE); |
| mIntentReviewLayout.setVisibility(View.GONE); |
| |
| mMode = MODE_CAPTURE; |
| } |
| |
| /** |
| * Perform a transition from the bottom bar options layout to the bottom bar |
| * capture layout. |
| */ |
| public void transitionToCancel() { |
| mCaptureLayout.setVisibility(View.GONE); |
| mIntentReviewLayout.setVisibility(View.GONE); |
| mCancelLayout.setVisibility(View.VISIBLE); |
| |
| mMode = MODE_CANCEL; |
| } |
| |
| /** |
| * Perform a transition to the global intent layout. The current layout |
| * state of the bottom bar is irrelevant. |
| */ |
| public void transitionToIntentCaptureLayout() { |
| mIntentReviewLayout.setVisibility(View.GONE); |
| mCaptureLayout.setVisibility(View.VISIBLE); |
| mCancelLayout.setVisibility(View.GONE); |
| |
| mMode = MODE_INTENT; |
| } |
| |
| /** |
| * Perform a transition to the global intent review layout. The current |
| * layout state of the bottom bar is irrelevant. |
| */ |
| public void transitionToIntentReviewLayout() { |
| mCaptureLayout.setVisibility(View.GONE); |
| mIntentReviewLayout.setVisibility(View.VISIBLE); |
| mCancelLayout.setVisibility(View.GONE); |
| |
| mMode = MODE_INTENT_REVIEW; |
| } |
| |
| /** |
| * @return whether UI is in intent review mode |
| */ |
| public boolean isInIntentReview() { |
| return mMode == MODE_INTENT_REVIEW; |
| } |
| |
| private void setButtonImageLevels(int level) { |
| ((ImageButton) findViewById(R.id.cancel_button)).setImageLevel(level); |
| ((ImageButton) findViewById(R.id.done_button)).setImageLevel(level); |
| ((ImageButton) findViewById(R.id.retake_button)).setImageLevel(level); |
| } |
| |
| private void setOverlayBottomBar(boolean overlay) { |
| mOverLayBottomBar = overlay; |
| if (overlay) { |
| setBackgroundAlpha(mBackgroundAlphaOverlay); |
| setButtonImageLevels(1); |
| } else { |
| setBackgroundAlpha(mBackgroundAlphaDefault); |
| setButtonImageLevels(0); |
| } |
| } |
| |
| /** |
| * Sets a capture layout helper to query layout rect from. |
| */ |
| public void setCaptureLayoutHelper(CaptureLayoutHelper helper) { |
| mCaptureLayoutHelper = helper; |
| } |
| |
| @Override |
| public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final int measureWidth = MeasureSpec.getSize(widthMeasureSpec); |
| final int measureHeight = MeasureSpec.getSize(heightMeasureSpec); |
| if (measureWidth == 0 || measureHeight == 0) { |
| return; |
| } |
| |
| if (mCaptureLayoutHelper == null) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| Log.e(TAG, "Capture layout helper needs to be set first."); |
| } else { |
| RectF bottomBarRect = mCaptureLayoutHelper.getBottomBarRect(); |
| super.onMeasure(MeasureSpec.makeMeasureSpec( |
| (int) bottomBarRect.width(), MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec((int) bottomBarRect.height(), MeasureSpec.EXACTLY) |
| ); |
| boolean shouldOverlayBottomBar = mCaptureLayoutHelper.shouldOverlayBottomBar(); |
| setOverlayBottomBar(shouldOverlayBottomBar); |
| } |
| } |
| |
| // prevent touches on bottom bar (not its children) |
| // from triggering a touch event on preview area |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| return true; |
| } |
| |
| @Override |
| public void setBackgroundColor(int color) { |
| mBackgroundColor = color; |
| setPaintColor(mBackgroundAlpha, mBackgroundColor); |
| setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor); |
| } |
| |
| private void setBackgroundPressedColor(int color) { |
| if (ApiHelper.isLOrHigher()) { |
| // not supported (setting a color on a RippleDrawable is hard =[ ) |
| } else { |
| mBackgroundPressedColor = color; |
| } |
| } |
| |
| private void setupShutterBackgroundForModeIndex(int index) { |
| LayerDrawable shutterBackground = mShutterButtonBackgrounds[index]; |
| mShutterButton.setBackground(shutterBackground); |
| |
| Drawable d = shutterBackground.getDrawable(0); |
| mAnimatedCircleDrawable = null; |
| mColorDrawable = null; |
| if (d instanceof AnimatedCircleDrawable) { |
| mAnimatedCircleDrawable = (AnimatedCircleDrawable) d; |
| } else if (d instanceof ColorDrawable) { |
| mColorDrawable = (ColorDrawable) d; |
| } |
| |
| int colorId = CameraUtil.getCameraThemeColorId(index, getContext()); |
| int pressedColor = getContext().getResources().getColor(colorId); |
| setBackgroundPressedColor(pressedColor); |
| refreshPaintColor(); |
| } |
| |
| public void setColorsForModeIndex(int index) { |
| setupShutterBackgroundForModeIndex(index); |
| } |
| |
| public void setBackgroundAlpha(int alpha) { |
| mBackgroundAlpha = alpha; |
| setPaintColor(mBackgroundAlpha, mBackgroundColor); |
| setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor); |
| } |
| |
| /** |
| * Sets the shutter button enabled if true, disabled if false. |
| * <p> |
| * Disabled means that the shutter button is not clickable and is greyed |
| * out. |
| */ |
| public void setShutterButtonEnabled(final boolean enabled) { |
| mShutterButton.post(new Runnable() { |
| @Override |
| public void run() { |
| mShutterButton.setEnabled(enabled); |
| setShutterButtonImportantToA11y(enabled); |
| } |
| }); |
| } |
| |
| /** |
| * Sets whether shutter button should be included in a11y announcement and |
| * navigation |
| */ |
| public void setShutterButtonImportantToA11y(boolean important) { |
| if (important) { |
| mShutterButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); |
| } else { |
| mShutterButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); |
| } |
| } |
| |
| /** |
| * Returns whether the capture button is enabled. |
| */ |
| public boolean isShutterButtonEnabled() { |
| return mShutterButton.isEnabled(); |
| } |
| |
| private TransitionDrawable crossfadeDrawable(Drawable from, Drawable to) { |
| Drawable[] arrayDrawable = new Drawable[2]; |
| arrayDrawable[0] = from; |
| arrayDrawable[1] = to; |
| TransitionDrawable transitionDrawable = new TransitionDrawable(arrayDrawable); |
| transitionDrawable.setCrossFadeEnabled(true); |
| return transitionDrawable; |
| } |
| |
| /** |
| * Sets the shutter button's icon resource. By default, all drawables |
| * instances loaded from the same resource share a common state; if you |
| * modify the state of one instance, all the other instances will receive |
| * the same modification. In order to modify properties of this icon |
| * drawable without affecting other drawables, here we use a mutable |
| * drawable which is guaranteed to not share states with other drawables. |
| */ |
| public void setShutterButtonIcon(int resId) { |
| Drawable iconDrawable = getResources().getDrawable(resId); |
| if (iconDrawable != null) { |
| iconDrawable = iconDrawable.mutate(); |
| } |
| mShutterButton.setImageDrawable(iconDrawable); |
| } |
| |
| /** |
| * Animates bar to a single stop button |
| */ |
| public void animateToVideoStop(int resId) { |
| if (mOverLayBottomBar && mAnimatedCircleDrawable != null) { |
| mAnimatedCircleDrawable.animateToSmallRadius(); |
| mDrawCircle = true; |
| } |
| |
| TransitionDrawable transitionDrawable = crossfadeDrawable( |
| mShutterButton.getDrawable(), |
| getResources().getDrawable(resId)); |
| mShutterButton.setImageDrawable(transitionDrawable); |
| transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); |
| } |
| |
| /** |
| * Animates bar to full width / length with video capture icon |
| */ |
| public void animateToFullSize(int resId) { |
| if (mDrawCircle && mAnimatedCircleDrawable != null) { |
| mAnimatedCircleDrawable.animateToFullSize(); |
| mDrawCircle = false; |
| } |
| |
| TransitionDrawable transitionDrawable = crossfadeDrawable( |
| mShutterButton.getDrawable(), |
| getResources().getDrawable(resId)); |
| mShutterButton.setImageDrawable(transitionDrawable); |
| transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); |
| } |
| } |