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