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