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