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