1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.camera.widget; 18 19 import android.animation.Animator; 20 import android.animation.ObjectAnimator; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.Canvas; 24 import android.graphics.drawable.Drawable; 25 import android.util.AttributeSet; 26 import android.view.OrientationEventListener; 27 import android.view.View; 28 29 import com.android.camera.util.CameraUtil; 30 import com.android.camera2.R; 31 32 import java.lang.ref.WeakReference; 33 34 /** 35 * This class is designed to show the video recording hint when device is held in 36 * portrait before video recording. The rotation device indicator will start rotating 37 * after a time-out and will fade out if the device is rotated to landscape. A tap 38 * on screen will dismiss the indicator. 39 */ 40 public class VideoRecordingHints extends View { 41 42 private static final int PORTRAIT_ROTATE_DELAY_MS = 1000; 43 private static final int ROTATION_DURATION_MS = 1000; 44 private static final int FADE_OUT_DURATION_MS = 600; 45 private static final float ROTATION_DEGREES = 180f; 46 private static final float INITIAL_ROTATION = 0f; 47 private static final int UNSET = -1; 48 49 private final int mRotateArrowsHalfSize; 50 private final int mPhoneGraphicHalfWidth; 51 private final Drawable mRotateArrows; 52 private final Drawable mPhoneGraphic; 53 private final int mPhoneGraphicHalfHeight; 54 private final boolean mIsDefaultToPortrait; 55 private float mRotation = INITIAL_ROTATION; 56 private final ValueAnimator mRotationAnimation; 57 private final ObjectAnimator mAlphaAnimator; 58 private boolean mIsInLandscape = false; 59 private int mCenterX = UNSET; 60 private int mCenterY = UNSET; 61 private int mLastOrientation = OrientationEventListener.ORIENTATION_UNKNOWN; 62 63 private static class RotationAnimatorListener implements Animator.AnimatorListener { 64 private final WeakReference<VideoRecordingHints> mHints; 65 private boolean mCanceled = false; 66 RotationAnimatorListener(VideoRecordingHints hint)67 public RotationAnimatorListener(VideoRecordingHints hint) { 68 mHints = new WeakReference<VideoRecordingHints>(hint); 69 } 70 71 @Override onAnimationStart(Animator animation)72 public void onAnimationStart(Animator animation) { 73 mCanceled = false; 74 } 75 76 @Override onAnimationEnd(Animator animation)77 public void onAnimationEnd(Animator animation) { 78 VideoRecordingHints hint = mHints.get(); 79 if (hint == null) { 80 return; 81 } 82 83 hint.mRotation = ((int) hint.mRotation) % 360; 84 // If animation is canceled, do not restart it. 85 if (mCanceled) { 86 return; 87 } 88 hint.post(new Runnable() { 89 @Override 90 public void run() { 91 VideoRecordingHints hint = mHints.get(); 92 if (hint != null) { 93 hint.continueRotationAnimation(); 94 } 95 } 96 }); 97 } 98 99 @Override onAnimationCancel(Animator animation)100 public void onAnimationCancel(Animator animation) { 101 mCanceled = true; 102 } 103 104 @Override onAnimationRepeat(Animator animation)105 public void onAnimationRepeat(Animator animation) { 106 // Do nothing. 107 } 108 } 109 110 private static class AlphaAnimatorListener implements Animator.AnimatorListener { 111 private final WeakReference<VideoRecordingHints> mHints; AlphaAnimatorListener(VideoRecordingHints hint)112 AlphaAnimatorListener(VideoRecordingHints hint) { 113 mHints = new WeakReference<VideoRecordingHints>(hint); 114 } 115 116 @Override onAnimationStart(Animator animation)117 public void onAnimationStart(Animator animation) { 118 // Do nothing. 119 } 120 121 @Override onAnimationEnd(Animator animation)122 public void onAnimationEnd(Animator animation) { 123 VideoRecordingHints hint = mHints.get(); 124 if (hint == null) { 125 return; 126 } 127 128 hint.invalidate(); 129 hint.setAlpha(1f); 130 hint.mRotation = 0; 131 } 132 133 @Override onAnimationCancel(Animator animation)134 public void onAnimationCancel(Animator animation) { 135 // Do nothing. 136 } 137 138 @Override onAnimationRepeat(Animator animation)139 public void onAnimationRepeat(Animator animation) { 140 // Do nothing. 141 } 142 } 143 VideoRecordingHints(Context context, AttributeSet attrs)144 public VideoRecordingHints(Context context, AttributeSet attrs) { 145 super(context, attrs); 146 mRotateArrows = getResources().getDrawable(R.drawable.rotate_arrows); 147 mPhoneGraphic = getResources().getDrawable(R.drawable.ic_phone_graphic); 148 mRotateArrowsHalfSize = getResources().getDimensionPixelSize( 149 R.dimen.video_hint_arrow_size) / 2; 150 mPhoneGraphicHalfWidth = getResources() 151 .getDimensionPixelSize(R.dimen.video_hint_phone_graphic_width) / 2; 152 mPhoneGraphicHalfHeight = getResources() 153 .getDimensionPixelSize(R.dimen.video_hint_phone_graphic_height) / 2; 154 155 mRotationAnimation = ValueAnimator.ofFloat(mRotation, mRotation + ROTATION_DEGREES); 156 mRotationAnimation.setDuration(ROTATION_DURATION_MS); 157 mRotationAnimation.setStartDelay(PORTRAIT_ROTATE_DELAY_MS); 158 mRotationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 159 @Override 160 public void onAnimationUpdate(ValueAnimator animation) { 161 mRotation = (Float) animation.getAnimatedValue(); 162 invalidate(); 163 } 164 }); 165 166 mRotationAnimation.addListener(new RotationAnimatorListener(this)); 167 168 mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f); 169 mAlphaAnimator.setDuration(FADE_OUT_DURATION_MS); 170 mAlphaAnimator.addListener(new AlphaAnimatorListener(this)); 171 mIsDefaultToPortrait = CameraUtil.isDefaultToPortrait(context); 172 } 173 174 /** 175 * Restart the rotation animation using the current rotation as the starting 176 * rotation, and then rotate a pre-defined amount. If the rotation animation 177 * is currently running, do nothing. 178 */ continueRotationAnimation()179 private void continueRotationAnimation() { 180 if (mRotationAnimation.isRunning()) { 181 return; 182 } 183 mRotationAnimation.setFloatValues(mRotation, mRotation + ROTATION_DEGREES); 184 mRotationAnimation.start(); 185 } 186 187 @Override onVisibilityChanged(View v, int visibility)188 public void onVisibilityChanged(View v, int visibility) { 189 super.onVisibilityChanged(v, visibility); 190 if (getVisibility() == VISIBLE && !isInLandscape()) { 191 continueRotationAnimation(); 192 } else if (getVisibility() != VISIBLE) { 193 mRotationAnimation.cancel(); 194 mRotation = 0; 195 } 196 } 197 198 @Override onLayout(boolean changed, int left, int top, int right, int bottom)199 public void onLayout(boolean changed, int left, int top, int right, int bottom) { 200 super.onLayout(changed, left, top, right, bottom); 201 // Center drawables in the layout 202 mCenterX = (right - left) / 2; 203 mCenterY = (bottom - top) / 2; 204 mRotateArrows.setBounds(mCenterX - mRotateArrowsHalfSize, mCenterY - mRotateArrowsHalfSize, 205 mCenterX + mRotateArrowsHalfSize, mCenterY + mRotateArrowsHalfSize); 206 mPhoneGraphic.setBounds(mCenterX - mPhoneGraphicHalfWidth, mCenterY - mPhoneGraphicHalfHeight, 207 mCenterX + mPhoneGraphicHalfWidth, mCenterY + mPhoneGraphicHalfHeight); 208 invalidate(); 209 } 210 211 @Override draw(Canvas canvas)212 public void draw(Canvas canvas) { 213 super.draw(canvas); 214 // Don't draw anything after the fade-out animation in landscape. 215 if (mIsInLandscape && !mAlphaAnimator.isRunning()) { 216 return; 217 } 218 canvas.save(); 219 canvas.rotate(-mRotation, mCenterX, mCenterY); 220 mRotateArrows.draw(canvas); 221 canvas.restore(); 222 if (mIsInLandscape) { 223 canvas.save(); 224 canvas.rotate(90, mCenterX, mCenterY); 225 mPhoneGraphic.draw(canvas); 226 canvas.restore(); 227 } else { 228 mPhoneGraphic.draw(canvas); 229 } 230 } 231 232 /** 233 * Handles orientation change by starting/stopping the video hint based on the 234 * new orientation. 235 */ onOrientationChanged(int orientation)236 public void onOrientationChanged(int orientation) { 237 if (mLastOrientation == orientation) { 238 return; 239 } 240 mLastOrientation = orientation; 241 if (mLastOrientation == OrientationEventListener.ORIENTATION_UNKNOWN) { 242 return; 243 } 244 245 mIsInLandscape = isInLandscape(); 246 if (getVisibility() == VISIBLE) { 247 if (mIsInLandscape) { 248 // Landscape. 249 mRotationAnimation.cancel(); 250 // Start fading out. 251 if (mAlphaAnimator.isRunning()) { 252 return; 253 } 254 mAlphaAnimator.start(); 255 } else { 256 // Portrait. 257 continueRotationAnimation(); 258 } 259 } 260 } 261 262 /** 263 * Returns whether the device is in landscape based on the natural orientation 264 * and rotation from natural orientation. 265 */ isInLandscape()266 private boolean isInLandscape() { 267 return (mLastOrientation % 180 == 90 && mIsDefaultToPortrait) 268 || (mLastOrientation % 180 == 0 && !mIsDefaultToPortrait); 269 } 270 } 271