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