1 /*
2  * Copyright (C) 2013 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.TimeInterpolator;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.graphics.ColorFilter;
25 import android.graphics.Paint;
26 import android.graphics.PixelFormat;
27 import android.graphics.drawable.Drawable;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.util.AttributeSet;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.animation.AnimationUtils;
34 import android.widget.FrameLayout;
35 
36 import com.android.camera.filmstrip.FilmstripContentPanel;
37 import com.android.camera.filmstrip.FilmstripController;
38 import com.android.camera.ui.FilmstripGestureRecognizer;
39 import com.android.camera.util.ApiHelper;
40 import com.android.camera.util.Gusterpolator;
41 import com.android.camera2.R;
42 
43 /**
44  * A {@link android.widget.FrameLayout} used for the parent layout of a
45  * {@link com.android.camera.widget.FilmstripView} to support animating in/out the
46  * filmstrip.
47  */
48 public class FilmstripLayout extends FrameLayout implements FilmstripContentPanel {
49 
50     private static final long DEFAULT_DURATION_MS = 250;
51     /**
52      *  If the fling velocity exceeds this threshold, open filmstrip at a constant
53      *  speed. Unit: pixel/ms.
54      */
55     private static final float FLING_VELOCITY_THRESHOLD = 4.0f;
56 
57     /**
58      * The layout containing the {@link com.android.camera.widget.FilmstripView}
59      * and other controls.
60      */
61     private FrameLayout mFilmstripContentLayout;
62     private FilmstripView mFilmstripView;
63     private FilmstripGestureRecognizer mGestureRecognizer;
64     private FilmstripGestureRecognizer.Listener mFilmstripGestureListener;
65     private final ValueAnimator mFilmstripAnimator = ValueAnimator.ofFloat(null);
66     private int mSwipeTrend;
67     private FilmstripBackground mBackgroundDrawable;
68     private Handler mHandler;
69     // We use this to record the current translation position instead of using
70     // the real value because we might set the translation before onMeasure()
71     // thus getMeasuredWidth() can be 0.
72     private float mFilmstripContentTranslationProgress;
73 
74     private Animator.AnimatorListener mFilmstripAnimatorListener = new Animator.AnimatorListener() {
75         private boolean mCanceled;
76 
77         @Override
78         public void onAnimationStart(Animator animator) {
79             mCanceled = false;
80         }
81 
82         @Override
83         public void onAnimationEnd(Animator animator) {
84             if (!mCanceled) {
85                 if (mFilmstripContentTranslationProgress != 0f) {
86                     mFilmstripView.getController().goToFilmstrip();
87                     setVisibility(INVISIBLE);
88                 } else {
89                     notifyShown();
90                 }
91             }
92         }
93 
94         @Override
95         public void onAnimationCancel(Animator animator) {
96             mCanceled = true;
97         }
98 
99         @Override
100         public void onAnimationRepeat(Animator animator) {
101             // Nothing.
102         }
103     };
104 
105     private ValueAnimator.AnimatorUpdateListener mFilmstripAnimatorUpdateListener =
106             new ValueAnimator.AnimatorUpdateListener() {
107                 @Override
108                 public void onAnimationUpdate(ValueAnimator valueAnimator) {
109                     translateContentLayout((Float) valueAnimator.getAnimatedValue());
110                     mBackgroundDrawable.invalidateSelf();
111                 }
112             };
113     private Listener mListener;
114 
FilmstripLayout(Context context)115     public FilmstripLayout(Context context) {
116         super(context);
117         init(context);
118     }
119 
FilmstripLayout(Context context, AttributeSet attrs)120     public FilmstripLayout(Context context, AttributeSet attrs) {
121         super(context, attrs);
122         init(context);
123     }
124 
FilmstripLayout(Context context, AttributeSet attrs, int defStyle)125     public FilmstripLayout(Context context, AttributeSet attrs, int defStyle) {
126         super(context, attrs, defStyle);
127         init(context);
128     }
129 
init(Context context)130     private void init(Context context) {
131         mGestureRecognizer = new FilmstripGestureRecognizer(context, new OpenFilmstripGesture());
132         mFilmstripAnimator.setDuration(DEFAULT_DURATION_MS);
133         TimeInterpolator interpolator;
134         if (ApiHelper.isLOrHigher()) {
135             interpolator = AnimationUtils.loadInterpolator(
136                     getContext(), android.R.interpolator.fast_out_slow_in);
137         } else {
138             interpolator = Gusterpolator.INSTANCE;
139         }
140         mFilmstripAnimator.setInterpolator(interpolator);
141         mFilmstripAnimator.addUpdateListener(mFilmstripAnimatorUpdateListener);
142         mFilmstripAnimator.addListener(mFilmstripAnimatorListener);
143         mHandler = new Handler(Looper.getMainLooper());
144         mBackgroundDrawable = new FilmstripBackground();
145         mBackgroundDrawable.setCallback(new Drawable.Callback() {
146             @Override
147             public void invalidateDrawable(Drawable drawable) {
148                 FilmstripLayout.this.invalidate();
149             }
150 
151             @Override
152             public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) {
153                 mHandler.postAtTime(runnable, drawable, l);
154             }
155 
156             @Override
157             public void unscheduleDrawable(Drawable drawable, Runnable runnable) {
158                 mHandler.removeCallbacks(runnable, drawable);
159             }
160         });
161         setBackground(mBackgroundDrawable);
162     }
163 
164     @Override
setFilmstripListener(Listener listener)165     public void setFilmstripListener(Listener listener) {
166         mListener = listener;
167         if (getVisibility() == VISIBLE && mFilmstripContentTranslationProgress == 0f) {
168             notifyShown();
169         } else {
170             if (getVisibility() != VISIBLE) {
171                 notifyHidden();
172             }
173         }
174         mFilmstripView.getController().setListener(listener);
175     }
176 
177     @Override
hide()178     public void hide() {
179         translateContentLayout(1f);
180         mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator);
181     }
182 
183     @Override
show()184     public void show() {
185         translateContentLayout(0f);
186         mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator);
187     }
188 
189     @Override
setVisibility(int visibility)190     public void setVisibility(int visibility) {
191         super.setVisibility(visibility);
192         if (visibility != VISIBLE) {
193             notifyHidden();
194         }
195     }
196 
notifyHidden()197     private void notifyHidden() {
198         if (mListener == null) {
199             return;
200         }
201         mListener.onFilmstripHidden();
202     }
203 
notifyShown()204     private void notifyShown() {
205         if (mListener == null) {
206             return;
207         }
208         mListener.onFilmstripShown();
209         mFilmstripView.zoomAtIndexChanged();
210         FilmstripController controller = mFilmstripView.getController();
211         int currentId = controller.getCurrentAdapterIndex();
212         if (controller.inFilmstrip()) {
213             mListener.onEnterFilmstrip(currentId);
214         } else if (controller.inFullScreen()) {
215             mListener.onEnterFullScreenUiShown(currentId);
216         }
217     }
218 
219     @Override
onLayout(boolean changed, int l, int t, int r, int b)220     public void onLayout(boolean changed, int l, int t, int r, int b) {
221         super.onLayout(changed, l, t, r, b);
222         if (changed && mFilmstripView != null && getVisibility() == INVISIBLE) {
223             hide();
224         } else {
225             translateContentLayout(mFilmstripContentTranslationProgress);
226         }
227     }
228 
229     @Override
onTouchEvent(MotionEvent ev)230     public boolean onTouchEvent(MotionEvent ev) {
231         return mGestureRecognizer.onTouchEvent(ev);
232     }
233 
234     @Override
onInterceptTouchEvent(MotionEvent ev)235     public boolean onInterceptTouchEvent(MotionEvent ev) {
236         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
237             // TODO: Remove this after the touch flow refactor is done in
238             // MainAtivityLayout.
239             getParent().requestDisallowInterceptTouchEvent(true);
240         }
241         return false;
242     }
243 
244     @Override
onFinishInflate()245     public void onFinishInflate() {
246         mFilmstripView = (FilmstripView) findViewById(R.id.filmstrip_view);
247         mFilmstripView.setOnTouchListener(new OnTouchListener() {
248 
249             @Override
250             public boolean onTouch(View view, MotionEvent motionEvent) {
251                 // Adjust the coordinates back since they are relative to the
252                 // child view.
253                 motionEvent.setLocation(motionEvent.getX() + mFilmstripContentLayout.getX(),
254                         motionEvent.getY() + mFilmstripContentLayout.getY());
255                 mGestureRecognizer.onTouchEvent(motionEvent);
256                 return true;
257             }
258         });
259         mFilmstripGestureListener = mFilmstripView.getGestureListener();
260         mFilmstripContentLayout = (FrameLayout) findViewById(R.id.camera_filmstrip_content_layout);
261     }
262 
263     @Override
onBackPressed()264     public boolean onBackPressed() {
265         return animateHide();
266     }
267 
268     @Override
animateHide()269     public boolean animateHide() {
270         if (getVisibility() == VISIBLE) {
271             if (!mFilmstripAnimator.isRunning()) {
272                 hideFilmstrip();
273             }
274             return true;
275         }
276         return false;
277     }
278 
hideFilmstrip()279     public void hideFilmstrip() {
280         // run the same view show/hides and animations
281         // that happen with a swipe gesture.
282         onSwipeOutBegin();
283         runAnimation(mFilmstripContentTranslationProgress, 1f);
284     }
285 
showFilmstrip()286     public void showFilmstrip() {
287         setVisibility(VISIBLE);
288         runAnimation(mFilmstripContentTranslationProgress, 0f);
289     }
290 
runAnimation(float begin, float end)291     private void runAnimation(float begin, float end) {
292         if (mFilmstripAnimator.isRunning()) {
293             return;
294         }
295         if (begin == end) {
296             // No need to start animation.
297             mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator);
298             return;
299         }
300         mFilmstripAnimator.setFloatValues(begin, end);
301         mFilmstripAnimator.start();
302     }
303 
translateContentLayout(float fraction)304     private void translateContentLayout(float fraction) {
305         mFilmstripContentTranslationProgress = fraction;
306         mFilmstripContentLayout.setTranslationX(fraction * getMeasuredWidth());
307     }
308 
translateContentLayoutByPixel(float pixel)309     private void translateContentLayoutByPixel(float pixel) {
310         mFilmstripContentLayout.setTranslationX(pixel);
311         mFilmstripContentTranslationProgress = pixel / getMeasuredWidth();
312     }
313 
onSwipeOut()314     private void onSwipeOut() {
315         if (mListener != null) {
316             mListener.onSwipeOut();
317         }
318     }
319 
onSwipeOutBegin()320     private void onSwipeOutBegin() {
321         if (mListener != null) {
322             mListener.onSwipeOutBegin();
323         }
324     }
325 
326     /**
327      * A gesture listener which passes all the gestures to the
328      * {@code mFilmstripView} by default and only intercepts scroll gestures
329      * when the {@code mFilmstripView} is not in full-screen.
330      */
331     private class OpenFilmstripGesture implements FilmstripGestureRecognizer.Listener {
332         @Override
onScroll(float x, float y, float dx, float dy)333         public boolean onScroll(float x, float y, float dx, float dy) {
334             if (mFilmstripView.getController().getCurrentAdapterIndex() == -1) {
335                 return true;
336             }
337             if (mFilmstripAnimator.isRunning()) {
338                 return true;
339             }
340             if (mFilmstripContentLayout.getTranslationX() == 0f &&
341                     mFilmstripGestureListener.onScroll(x, y, dx, dy)) {
342                 return true;
343             }
344             mSwipeTrend = (((int) dx) >> 1) + (mSwipeTrend >> 1);
345             if (dx < 0 && mFilmstripContentLayout.getTranslationX() == 0) {
346                 mBackgroundDrawable.setOffset(0);
347                 FilmstripLayout.this.onSwipeOutBegin();
348             }
349 
350             // When we start translating the filmstrip in, we want the left edge of the
351             // first view to always be at the rightmost edge of the screen so that it
352             // appears instantly, regardless of the view's distance from the edge of the
353             // filmstrip view. To do so, on our first translation, jump the filmstrip view
354             // to the correct position, and then smoothly animate the translation from that
355             // initial point.
356             if (dx > 0 && mFilmstripContentLayout.getTranslationX() == getMeasuredWidth()) {
357                 final int currentItemLeft = mFilmstripView.getCurrentItemLeft();
358                 dx = currentItemLeft;
359                 mBackgroundDrawable.setOffset(currentItemLeft);
360             }
361 
362             float translate = mFilmstripContentLayout.getTranslationX() - dx;
363             if (translate < 0f) {
364                 translate = 0f;
365             } else {
366                 if (translate > getMeasuredWidth()) {
367                     translate = getMeasuredWidth();
368                 }
369             }
370             translateContentLayoutByPixel(translate);
371             if (translate == 0 && dx > 0) {
372                 // This will only happen once since when this condition holds
373                 // the onScroll() callback will be forwarded to the filmstrip
374                 // view.
375                 mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator);
376             }
377             mBackgroundDrawable.invalidateSelf();
378             return true;
379         }
380 
381         @Override
onMouseScroll(float hscroll, float vscroll)382         public boolean onMouseScroll(float hscroll, float vscroll) {
383             if (mFilmstripContentTranslationProgress == 0f) {
384                 return mFilmstripGestureListener.onMouseScroll(hscroll, vscroll);
385             }
386             return false;
387         }
388 
389         @Override
onSingleTapUp(float x, float y)390         public boolean onSingleTapUp(float x, float y) {
391             if (mFilmstripContentTranslationProgress == 0f) {
392                 return mFilmstripGestureListener.onSingleTapUp(x, y);
393             }
394             return false;
395         }
396 
397         @Override
onDoubleTap(float x, float y)398         public boolean onDoubleTap(float x, float y) {
399             if (mFilmstripContentTranslationProgress == 0f) {
400                 return mFilmstripGestureListener.onDoubleTap(x, y);
401             }
402             return false;
403         }
404 
405         /**
406          * @param velocityX The fling velocity in the X direction.
407          * @return Whether the filmstrip should be opened,
408          * given velocityX and mSwipeTrend.
409          */
flingShouldOpenFilmstrip(float velocityX)410         private boolean flingShouldOpenFilmstrip(float velocityX) {
411             return (mSwipeTrend > 0) &&
412                     (velocityX < 0.0f) &&
413                     (Math.abs(velocityX / 1000.0f) > FLING_VELOCITY_THRESHOLD);
414         }
415 
416         @Override
onFling(float velocityX, float velocityY)417         public boolean onFling(float velocityX, float velocityY) {
418             if (mFilmstripContentTranslationProgress == 0f) {
419                 return mFilmstripGestureListener.onFling(velocityX, velocityY);
420             } else if (flingShouldOpenFilmstrip(velocityX)) {
421                 showFilmstrip();
422                 return true;
423             }
424 
425             return false;
426         }
427 
428         @Override
onScaleBegin(float focusX, float focusY)429         public boolean onScaleBegin(float focusX, float focusY) {
430             if (mFilmstripContentTranslationProgress == 0f) {
431                 return mFilmstripGestureListener.onScaleBegin(focusX, focusY);
432             }
433             return false;
434         }
435 
436         @Override
onScale(float focusX, float focusY, float scale)437         public boolean onScale(float focusX, float focusY, float scale) {
438             if (mFilmstripContentTranslationProgress == 0f) {
439                 return mFilmstripGestureListener.onScale(focusX, focusY, scale);
440             }
441             return false;
442         }
443 
444         @Override
onDown(float x, float y)445         public boolean onDown(float x, float y) {
446             if (mFilmstripContentLayout.getTranslationX() == 0f) {
447                 return mFilmstripGestureListener.onDown(x, y);
448             }
449             return false;
450         }
451 
452         @Override
onUp(float x, float y)453         public boolean onUp(float x, float y) {
454             if (mFilmstripContentLayout.getTranslationX() == 0f) {
455                 return mFilmstripGestureListener.onUp(x, y);
456             }
457             if (mSwipeTrend < 0) {
458                 hideFilmstrip();
459                 onSwipeOut();
460             } else {
461                 if (mFilmstripContentLayout.getTranslationX() >= getMeasuredWidth() / 2) {
462                     hideFilmstrip();
463                     onSwipeOut();
464                 } else {
465                     showFilmstrip();
466                 }
467             }
468             mSwipeTrend = 0;
469             return false;
470         }
471 
472         @Override
onLongPress(float x, float y)473         public void onLongPress(float x, float y) {
474             mFilmstripGestureListener.onLongPress(x, y);
475         }
476 
477         @Override
onScaleEnd()478         public void onScaleEnd() {
479             if (mFilmstripContentLayout.getTranslationX() == 0f) {
480                 mFilmstripGestureListener.onScaleEnd();
481             }
482         }
483     }
484 
485     private class FilmstripBackground extends Drawable {
486         private Paint mPaint;
487         private int mOffset;
488 
FilmstripBackground()489         public FilmstripBackground() {
490             mPaint = new Paint();
491             mPaint.setAntiAlias(true);
492             mPaint.setColor(getResources().getColor(R.color.camera_gray_background));
493             mPaint.setAlpha(255);
494         }
495 
496         /**
497          * Adjust the target width and translation calculation when we start translating
498          * from a point where width != translationX so that alpha scales smoothly.
499          */
setOffset(int offset)500         public void setOffset(int offset) {
501             mOffset = offset;
502         }
503 
504         @Override
setAlpha(int i)505         public void setAlpha(int i) {
506             mPaint.setAlpha(i);
507         }
508 
setAlpha(float a)509         private void setAlpha(float a) {
510             setAlpha((int) (a*255.0f));
511         }
512 
513         @Override
setColorFilter(ColorFilter colorFilter)514         public void setColorFilter(ColorFilter colorFilter) {
515             mPaint.setColorFilter(colorFilter);
516         }
517 
518         @Override
getOpacity()519         public int getOpacity() {
520             return PixelFormat.TRANSLUCENT;
521         }
522 
523         @Override
draw(Canvas canvas)524         public void draw(Canvas canvas) {
525             int width = getMeasuredWidth() - mOffset;
526             float translation = mFilmstripContentLayout.getTranslationX() - mOffset;
527             if (translation == width) {
528                 return;
529             }
530 
531             setAlpha(1.0f - mFilmstripContentTranslationProgress);
532             canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
533         }
534     }
535 }
536