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