Home | History | Annotate | Download | only in widget
      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 
    115     public FilmstripLayout(Context context) {
    116         super(context);
    117         init(context);
    118     }
    119 
    120     public FilmstripLayout(Context context, AttributeSet attrs) {
    121         super(context, attrs);
    122         init(context);
    123     }
    124 
    125     public FilmstripLayout(Context context, AttributeSet attrs, int defStyle) {
    126         super(context, attrs, defStyle);
    127         init(context);
    128     }
    129 
    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
    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
    178     public void hide() {
    179         translateContentLayout(1f);
    180         mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator);
    181     }
    182 
    183     @Override
    184     public void show() {
    185         translateContentLayout(0f);
    186         mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator);
    187     }
    188 
    189     @Override
    190     public void setVisibility(int visibility) {
    191         super.setVisibility(visibility);
    192         if (visibility != VISIBLE) {
    193             notifyHidden();
    194         }
    195     }
    196 
    197     private void notifyHidden() {
    198         if (mListener == null) {
    199             return;
    200         }
    201         mListener.onFilmstripHidden();
    202     }
    203 
    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
    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
    230     public boolean onTouchEvent(MotionEvent ev) {
    231         return mGestureRecognizer.onTouchEvent(ev);
    232     }
    233 
    234     @Override
    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
    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
    264     public boolean onBackPressed() {
    265         return animateHide();
    266     }
    267 
    268     @Override
    269     public boolean animateHide() {
    270         if (getVisibility() == VISIBLE) {
    271             if (!mFilmstripAnimator.isRunning()) {
    272                 hideFilmstrip();
    273             }
    274             return true;
    275         }
    276         return false;
    277     }
    278 
    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 
    286     public void showFilmstrip() {
    287         setVisibility(VISIBLE);
    288         runAnimation(mFilmstripContentTranslationProgress, 0f);
    289     }
    290 
    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 
    304     private void translateContentLayout(float fraction) {
    305         mFilmstripContentTranslationProgress = fraction;
    306         mFilmstripContentLayout.setTranslationX(fraction * getMeasuredWidth());
    307     }
    308 
    309     private void translateContentLayoutByPixel(float pixel) {
    310         mFilmstripContentLayout.setTranslationX(pixel);
    311         mFilmstripContentTranslationProgress = pixel / getMeasuredWidth();
    312     }
    313 
    314     private void onSwipeOut() {
    315         if (mListener != null) {
    316             mListener.onSwipeOut();
    317         }
    318     }
    319 
    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
    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
    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
    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
    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          */
    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
    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
    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
    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
    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
    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
    473         public void onLongPress(float x, float y) {
    474             mFilmstripGestureListener.onLongPress(x, y);
    475         }
    476 
    477         @Override
    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 
    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          */
    500         public void setOffset(int offset) {
    501             mOffset = offset;
    502         }
    503 
    504         @Override
    505         public void setAlpha(int i) {
    506             mPaint.setAlpha(i);
    507         }
    508 
    509         private void setAlpha(float a) {
    510             setAlpha((int) (a*255.0f));
    511         }
    512 
    513         @Override
    514         public void setColorFilter(ColorFilter colorFilter) {
    515             mPaint.setColorFilter(colorFilter);
    516         }
    517 
    518         @Override
    519         public int getOpacity() {
    520             return PixelFormat.TRANSLUCENT;
    521         }
    522 
    523         @Override
    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