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