Home | History | Annotate | Download | only in cardstream
      1 /*
      2 * Copyright 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 
     18 package com.example.android.batchstepsensor.cardstream;
     19 
     20 import android.animation.Animator;
     21 import android.animation.LayoutTransition;
     22 import android.animation.ObjectAnimator;
     23 import android.annotation.SuppressLint;
     24 import android.annotation.TargetApi;
     25 import android.content.Context;
     26 import android.content.res.TypedArray;
     27 import android.graphics.Rect;
     28 import android.os.Build;
     29 import android.util.AttributeSet;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewConfiguration;
     33 import android.view.ViewGroup;
     34 import android.view.ViewParent;
     35 import android.widget.LinearLayout;
     36 import android.widget.ScrollView;
     37 
     38 import com.example.android.common.logger.Log;
     39 import com.example.android.batchstepsensor.R;
     40 
     41 import java.util.ArrayList;
     42 
     43 /**
     44  * A Layout that contains a stream of card views.
     45  */
     46 public class CardStreamLinearLayout extends LinearLayout {
     47 
     48     public static final int ANIMATION_SPEED_SLOW = 1001;
     49     public static final int ANIMATION_SPEED_NORMAL = 1002;
     50     public static final int ANIMATION_SPEED_FAST = 1003;
     51 
     52     private static final String TAG = "CardStreamLinearLayout";
     53     private final ArrayList<View> mFixedViewList = new ArrayList<View>();
     54     private final Rect mChildRect = new Rect();
     55     private CardStreamAnimator mAnimators;
     56     private OnDissmissListener mDismissListener = null;
     57     private boolean mLayouted = false;
     58     private boolean mSwiping = false;
     59     private String mFirstVisibleCardTag = null;
     60     private boolean mShowInitialAnimation = false;
     61 
     62     /**
     63      * Handle touch events to fade/move dragged items as they are swiped out
     64      */
     65     private OnTouchListener mTouchListener = new OnTouchListener() {
     66 
     67         private float mDownX;
     68         private float mDownY;
     69 
     70         @Override
     71         public boolean onTouch(final View v, MotionEvent event) {
     72 
     73             switch (event.getAction()) {
     74                 case MotionEvent.ACTION_DOWN:
     75                     mDownX = event.getX();
     76                     mDownY = event.getY();
     77                     break;
     78                 case MotionEvent.ACTION_CANCEL:
     79                     resetAnimatedView(v);
     80                     mSwiping = false;
     81                     mDownX = 0.f;
     82                     mDownY = 0.f;
     83                     break;
     84                 case MotionEvent.ACTION_MOVE: {
     85 
     86                     float x = event.getX() + v.getTranslationX();
     87                     float y = event.getY() + v.getTranslationY();
     88 
     89                     mDownX = mDownX == 0.f ? x : mDownX;
     90                     mDownY = mDownY == 0.f ? x : mDownY;
     91 
     92                     float deltaX = x - mDownX;
     93                     float deltaY = y - mDownY;
     94 
     95                     if (!mSwiping && isSwiping(deltaX, deltaY)) {
     96                         mSwiping = true;
     97                         v.getParent().requestDisallowInterceptTouchEvent(true);
     98                     } else {
     99                         swipeView(v, deltaX, deltaY);
    100                     }
    101                 }
    102                 break;
    103                 case MotionEvent.ACTION_UP: {
    104                     // User let go - figure out whether to animate the view out, or back into place
    105                     if (mSwiping) {
    106                         float x = event.getX() + v.getTranslationX();
    107                         float y = event.getY() + v.getTranslationY();
    108 
    109                         float deltaX = x - mDownX;
    110                         float deltaY = y - mDownX;
    111                         float deltaXAbs = Math.abs(deltaX);
    112 
    113                         // User let go - figure out whether to animate the view out, or back into place
    114                         boolean remove = deltaXAbs > v.getWidth() / 4 && !isFixedView(v);
    115                         if( remove )
    116                             handleViewSwipingOut(v, deltaX, deltaY);
    117                         else
    118                             handleViewSwipingIn(v, deltaX, deltaY);
    119                     }
    120                     mDownX = 0.f;
    121                     mDownY = 0.f;
    122                     mSwiping = false;
    123                 }
    124                 break;
    125                 default:
    126                     return false;
    127             }
    128             return false;
    129         }
    130     };
    131     private int mSwipeSlop = -1;
    132     /**
    133      * Handle end-transition animation event of each child and launch a following animation.
    134      */
    135     private LayoutTransition.TransitionListener mTransitionListener
    136             = new LayoutTransition.TransitionListener() {
    137 
    138         @Override
    139         public void startTransition(LayoutTransition transition, ViewGroup container, View
    140                 view, int transitionType) {
    141             Log.d(TAG, "Start LayoutTransition animation:" + transitionType);
    142         }
    143 
    144         @Override
    145         public void endTransition(LayoutTransition transition, ViewGroup container,
    146                                   final View view, int transitionType) {
    147 
    148             Log.d(TAG, "End LayoutTransition animation:" + transitionType);
    149             if (transitionType == LayoutTransition.APPEARING) {
    150                 final View area = view.findViewById(R.id.card_actionarea);
    151                 if (area != null) {
    152                     runShowActionAreaAnimation(container, area);
    153                 }
    154             }
    155         }
    156     };
    157     /**
    158      * Handle a hierarchy change event
    159      * when a new child is added, scroll to bottom and hide action area..
    160      */
    161     private OnHierarchyChangeListener mOnHierarchyChangeListener
    162             = new OnHierarchyChangeListener() {
    163         @Override
    164         public void onChildViewAdded(final View parent, final View child) {
    165 
    166             Log.d(TAG, "child is added: " + child);
    167 
    168             ViewParent scrollView = parent.getParent();
    169             if (scrollView != null && scrollView instanceof ScrollView) {
    170                 ((ScrollView) scrollView).fullScroll(FOCUS_DOWN);
    171             }
    172 
    173             if (getLayoutTransition() != null) {
    174                 View view = child.findViewById(R.id.card_actionarea);
    175                 if (view != null)
    176                     view.setAlpha(0.f);
    177             }
    178         }
    179 
    180         @Override
    181         public void onChildViewRemoved(View parent, View child) {
    182             Log.d(TAG, "child is removed: " + child);
    183             mFixedViewList.remove(child);
    184         }
    185     };
    186     private int mLastDownX;
    187 
    188     public CardStreamLinearLayout(Context context) {
    189         super(context);
    190         initialize(null, 0);
    191     }
    192 
    193     public CardStreamLinearLayout(Context context, AttributeSet attrs) {
    194         super(context, attrs);
    195         initialize(attrs, 0);
    196     }
    197 
    198     @SuppressLint("NewApi")
    199     public CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle) {
    200         super(context, attrs, defStyle);
    201         initialize(attrs, defStyle);
    202     }
    203 
    204     /**
    205      * add a card view w/ canDismiss flag.
    206      *
    207      * @param cardView   a card view
    208      * @param canDismiss flag to indicate this card is dismissible or not.
    209      */
    210     public void addCard(View cardView, boolean canDismiss) {
    211         if (cardView.getParent() == null) {
    212             initCard(cardView, canDismiss);
    213 
    214             ViewGroup.LayoutParams param = cardView.getLayoutParams();
    215             if(param == null)
    216                 param = generateDefaultLayoutParams();
    217 
    218             super.addView(cardView, -1, param);
    219         }
    220     }
    221 
    222     @Override
    223     public void addView(View child, int index, ViewGroup.LayoutParams params) {
    224         if (child.getParent() == null) {
    225             initCard(child, true);
    226             super.addView(child, index, params);
    227         }
    228     }
    229 
    230     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    231     @Override
    232     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    233         super.onLayout(changed, l, t, r, b);
    234         Log.d(TAG, "onLayout: " + changed);
    235 
    236         if( changed && !mLayouted ){
    237             mLayouted = true;
    238 
    239             ObjectAnimator animator;
    240             LayoutTransition layoutTransition = new LayoutTransition();
    241 
    242             animator = mAnimators.getDisappearingAnimator(getContext());
    243             layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animator);
    244 
    245             animator = mAnimators.getAppearingAnimator(getContext());
    246             layoutTransition.setAnimator(LayoutTransition.APPEARING, animator);
    247 
    248             layoutTransition.addTransitionListener(mTransitionListener);
    249 
    250             if( animator != null )
    251                 layoutTransition.setDuration(animator.getDuration());
    252 
    253             setLayoutTransition(layoutTransition);
    254 
    255             if( mShowInitialAnimation )
    256                 runInitialAnimations();
    257 
    258             if (mFirstVisibleCardTag != null) {
    259                 scrollToCard(mFirstVisibleCardTag);
    260                 mFirstVisibleCardTag = null;
    261             }
    262         }
    263     }
    264 
    265     /**
    266      * Check whether a user moved enough distance to start a swipe action or not.
    267      *
    268      * @param deltaX
    269      * @param deltaY
    270      * @return true if a user is swiping.
    271      */
    272     protected boolean isSwiping(float deltaX, float deltaY) {
    273 
    274         if (mSwipeSlop < 0) {
    275             //get swipping slop from ViewConfiguration;
    276             mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    277         }
    278 
    279         boolean swipping = false;
    280         float absDeltaX = Math.abs(deltaX);
    281 
    282         if( absDeltaX > mSwipeSlop )
    283             return true;
    284 
    285         return swipping;
    286     }
    287 
    288     /**
    289      * Swipe a view by moving distance
    290      *
    291      * @param child a target view
    292      * @param deltaX x moving distance by x-axis.
    293      * @param deltaY y moving distance by y-axis.
    294      */
    295     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    296     protected void swipeView(View child, float deltaX, float deltaY) {
    297         if (isFixedView(child)){
    298             deltaX = deltaX / 4;
    299         }
    300 
    301         float deltaXAbs = Math.abs(deltaX);
    302         float fractionCovered = deltaXAbs / (float) child.getWidth();
    303 
    304         child.setTranslationX(deltaX);
    305         child.setAlpha(1.f - fractionCovered);
    306 
    307         if (deltaX > 0)
    308             child.setRotationY(-15.f * fractionCovered);
    309         else
    310             child.setRotationY(15.f * fractionCovered);
    311     }
    312 
    313     protected void notifyOnDismissEvent( View child ){
    314         if( child == null || mDismissListener == null )
    315             return;
    316 
    317         mDismissListener.onDismiss((String) child.getTag());
    318     }
    319 
    320     /**
    321      * get the tag of the first visible child in this layout
    322      *
    323      * @return tag of the first visible child or null
    324      */
    325     public String getFirstVisibleCardTag() {
    326 
    327         final int count = getChildCount();
    328 
    329         if (count == 0)
    330             return null;
    331 
    332         for (int index = 0; index < count; ++index) {
    333             //check the position of each view.
    334             View child = getChildAt(index);
    335             if (child.getGlobalVisibleRect(mChildRect) == true)
    336                 return (String) child.getTag();
    337         }
    338 
    339         return null;
    340     }
    341 
    342     /**
    343      * Set the first visible card of this linear layout.
    344      *
    345      * @param tag tag of a card which should already added to this layout.
    346      */
    347     public void setFirstVisibleCard(String tag) {
    348         if (tag == null)
    349             return; //do nothing.
    350 
    351         if (mLayouted) {
    352             scrollToCard(tag);
    353         } else {
    354             //keep the tag for next use.
    355             mFirstVisibleCardTag = tag;
    356         }
    357     }
    358 
    359     /**
    360      * If this flag is set,
    361      * after finishing initial onLayout event, an initial animation which is defined in DefaultCardStreamAnimator is launched.
    362      */
    363     public void triggerShowInitialAnimation(){
    364         mShowInitialAnimation = true;
    365     }
    366 
    367     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    368     public void setCardStreamAnimator( CardStreamAnimator animators ){
    369 
    370         if( animators == null )
    371             mAnimators = new CardStreamAnimator.EmptyAnimator();
    372         else
    373             mAnimators = animators;
    374 
    375         LayoutTransition layoutTransition = getLayoutTransition();
    376 
    377         if( layoutTransition != null ){
    378             layoutTransition.setAnimator( LayoutTransition.APPEARING,
    379                     mAnimators.getAppearingAnimator(getContext()) );
    380             layoutTransition.setAnimator( LayoutTransition.DISAPPEARING,
    381                     mAnimators.getDisappearingAnimator(getContext()) );
    382         }
    383     }
    384 
    385     /**
    386      * set a OnDismissListener which called when user dismiss a card.
    387      *
    388      * @param listener
    389      */
    390     public void setOnDismissListener(OnDissmissListener listener) {
    391         mDismissListener = listener;
    392     }
    393 
    394     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    395     private void initialize(AttributeSet attrs, int defStyle) {
    396 
    397         float speedFactor = 1.f;
    398 
    399         if (attrs != null) {
    400             TypedArray a = getContext().obtainStyledAttributes(attrs,
    401                     R.styleable.CardStream, defStyle, 0);
    402 
    403             if( a != null ){
    404                 int speedType = a.getInt(R.styleable.CardStream_animationDuration, 1001);
    405                 switch (speedType){
    406                     case ANIMATION_SPEED_FAST:
    407                         speedFactor = 0.5f;
    408                         break;
    409                     case ANIMATION_SPEED_NORMAL:
    410                         speedFactor = 1.f;
    411                         break;
    412                     case ANIMATION_SPEED_SLOW:
    413                         speedFactor = 2.f;
    414                         break;
    415                 }
    416 
    417                 String animatorName = a.getString(R.styleable.CardStream_animators);
    418 
    419                 try {
    420                     if( animatorName != null )
    421                         mAnimators = (CardStreamAnimator) getClass().getClassLoader()
    422                                 .loadClass(animatorName).newInstance();
    423                 } catch (Exception e) {
    424                     Log.e(TAG, "Fail to load animator:" + animatorName, e);
    425                 } finally {
    426                     if(mAnimators == null)
    427                         mAnimators = new DefaultCardStreamAnimator();
    428                 }
    429                 a.recycle();
    430             }
    431         }
    432 
    433         mAnimators.setSpeedFactor(speedFactor);
    434         mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    435         setOnHierarchyChangeListener(mOnHierarchyChangeListener);
    436     }
    437 
    438     private void initCard(View cardView, boolean canDismiss) {
    439         resetAnimatedView(cardView);
    440         cardView.setOnTouchListener(mTouchListener);
    441         if (!canDismiss)
    442             mFixedViewList.add(cardView);
    443     }
    444 
    445     private boolean isFixedView(View v) {
    446         return mFixedViewList.contains(v);
    447     }
    448 
    449     private void resetAnimatedView(View child) {
    450         child.setAlpha(1.f);
    451         child.setTranslationX(0.f);
    452         child.setTranslationY(0.f);
    453         child.setRotation(0.f);
    454         child.setRotationY(0.f);
    455         child.setRotationX(0.f);
    456         child.setScaleX(1.f);
    457         child.setScaleY(1.f);
    458     }
    459 
    460     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    461     private void runInitialAnimations() {
    462         if( mAnimators == null )
    463             return;
    464 
    465         final int count = getChildCount();
    466 
    467         for (int index = 0; index < count; ++index) {
    468             final View child = getChildAt(index);
    469             ObjectAnimator animator =  mAnimators.getInitalAnimator(getContext());
    470             if( animator != null ){
    471                 animator.setTarget(child);
    472                 animator.start();
    473             }
    474         }
    475     }
    476 
    477     private void runShowActionAreaAnimation(View parent, View area) {
    478         area.setPivotY(0.f);
    479         area.setPivotX(parent.getWidth() / 2.f);
    480 
    481         area.setAlpha(0.5f);
    482         area.setRotationX(-90.f);
    483         area.animate().rotationX(0.f).alpha(1.f).setDuration(400);
    484     }
    485 
    486     private void handleViewSwipingOut(final View child, float deltaX, float deltaY) {
    487         ObjectAnimator animator = mAnimators.getSwipeOutAnimator(child, deltaX, deltaY);
    488         if( animator != null ){
    489             animator.addListener(new EndAnimationWrapper() {
    490                 @Override
    491                 public void onAnimationEnd(Animator animation) {
    492                     removeView(child);
    493                     notifyOnDismissEvent(child);
    494                 }
    495             });
    496         } else {
    497             removeView(child);
    498             notifyOnDismissEvent(child);
    499         }
    500 
    501         if( animator != null ){
    502             animator.setTarget(child);
    503             animator.start();
    504         }
    505     }
    506 
    507     private void handleViewSwipingIn(final View child, float deltaX, float deltaY) {
    508         ObjectAnimator animator = mAnimators.getSwipeInAnimator(child, deltaX, deltaY);
    509         if( animator != null ){
    510             animator.addListener(new EndAnimationWrapper() {
    511                 @Override
    512                 public void onAnimationEnd(Animator animation) {
    513                     child.setTranslationY(0.f);
    514                     child.setTranslationX(0.f);
    515                 }
    516             });
    517         } else {
    518             child.setTranslationY(0.f);
    519             child.setTranslationX(0.f);
    520         }
    521 
    522         if( animator != null ){
    523             animator.setTarget(child);
    524             animator.start();
    525         }
    526     }
    527 
    528     private void scrollToCard(String tag) {
    529 
    530 
    531         final int count = getChildCount();
    532         for (int index = 0; index < count; ++index) {
    533             View child = getChildAt(index);
    534 
    535             if (tag.equals(child.getTag())) {
    536 
    537                 ViewParent parent = getParent();
    538                 if( parent != null && parent instanceof ScrollView ){
    539                     ((ScrollView)parent).smoothScrollTo(
    540                             0, child.getTop() - getPaddingTop() - child.getPaddingTop());
    541                 }
    542                 return;
    543             }
    544         }
    545     }
    546 
    547     public interface OnDissmissListener {
    548         public void onDismiss(String tag);
    549     }
    550 
    551     /**
    552      * Empty default AnimationListener
    553      */
    554     private abstract class EndAnimationWrapper implements Animator.AnimatorListener {
    555 
    556         @Override
    557         public void onAnimationStart(Animator animation) {
    558         }
    559 
    560         @Override
    561         public void onAnimationCancel(Animator animation) {
    562         }
    563 
    564         @Override
    565         public void onAnimationRepeat(Animator animation) {
    566         }
    567     }//end of inner class
    568 }
    569