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
}