Show navigation Hide navigation
BatchStepSensor / src / com.example.android.batchstepsensor / cardstream /

CardStreamLinearLayout.java

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
 
188
    public CardStreamLinearLayout(Context context) {
189
        super(context);
190
        initialize(null, 0);
191
    }
192
 
193
    public CardStreamLinearLayout(Context context, AttributeSet attrs) {
194
        super(context, attrs);
195
        initialize(attrs, 0);
196
    }
197
 
198
    @SuppressLint("NewApi")
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
     */
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
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
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
     */
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)
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
 
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
     */
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
     */
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
     */
363
    public void triggerShowInitialAnimation(){
364
        mShowInitialAnimation = true;
365
    }
366
 
367
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
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
     */
390
    public void setOnDismissListener(OnDissmissListener listener) {
391
        mDismissListener = listener;
392
    }
393
 
394
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
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
 
438
    private void initCard(View cardView, boolean canDismiss) {
439
        resetAnimatedView(cardView);
440
        cardView.setOnTouchListener(mTouchListener);
441
        if (!canDismiss)
442
            mFixedViewList.add(cardView);
443
    }
444
 
445
    private boolean isFixedView(View v) {
446
        return mFixedViewList.contains(v);
447
    }
448
 
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)
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
 
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
 
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
 
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
 
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 {
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
557
        public void onAnimationStart(Animator animation) {
558
        }
559
 
560
        @Override
561
        public void onAnimationCancel(Animator animation) {
562
        }
563
 
564
        @Override
565
        public void onAnimationRepeat(Animator animation) {
566
        }
567
    }//end of inner class
568
}