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 
CardStreamLinearLayout(Context context)188     public CardStreamLinearLayout(Context context) {
189         super(context);
190         initialize(null, 0);
191     }
192 
CardStreamLinearLayout(Context context, AttributeSet attrs)193     public CardStreamLinearLayout(Context context, AttributeSet attrs) {
194         super(context, attrs);
195         initialize(attrs, 0);
196     }
197 
198     @SuppressLint("NewApi")
CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle)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      */
addCard(View cardView, boolean canDismiss)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
addView(View child, int index, ViewGroup.LayoutParams params)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
onLayout(boolean changed, int l, int t, int r, int b)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      */
isSwiping(float deltaX, float deltaY)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)
swipeView(View child, float deltaX, float deltaY)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 
notifyOnDismissEvent( View child )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      */
getFirstVisibleCardTag()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      */
setFirstVisibleCard(String tag)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      */
triggerShowInitialAnimation()363     public void triggerShowInitialAnimation(){
364         mShowInitialAnimation = true;
365     }
366 
367     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
setCardStreamAnimator( CardStreamAnimator animators )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      */
setOnDismissListener(OnDissmissListener listener)390     public void setOnDismissListener(OnDissmissListener listener) {
391         mDismissListener = listener;
392     }
393 
394     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
initialize(AttributeSet attrs, int defStyle)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 
initCard(View cardView, boolean canDismiss)438     private void initCard(View cardView, boolean canDismiss) {
439         resetAnimatedView(cardView);
440         cardView.setOnTouchListener(mTouchListener);
441         if (!canDismiss)
442             mFixedViewList.add(cardView);
443     }
444 
isFixedView(View v)445     private boolean isFixedView(View v) {
446         return mFixedViewList.contains(v);
447     }
448 
resetAnimatedView(View child)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)
runInitialAnimations()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 
runShowActionAreaAnimation(View parent, View area)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 
handleViewSwipingOut(final View child, float deltaX, float deltaY)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 
handleViewSwipingIn(final View child, float deltaX, float deltaY)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 
scrollToCard(String tag)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 {
onDismiss(String tag)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
onAnimationStart(Animator animation)557         public void onAnimationStart(Animator animation) {
558         }
559 
560         @Override
onAnimationCancel(Animator animation)561         public void onAnimationCancel(Animator animation) {
562         }
563 
564         @Override
onAnimationRepeat(Animator animation)565         public void onAnimationRepeat(Animator animation) {
566         }
567     }//end of inner class
568 }
569