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 CardStreamLinearLayout(Context context)188 public CardStreamLinearLayout(Context context) { 189 super(context); 190 initialize(null, 0); 191 } 192 CardStreamLinearLayout(Context context, AttributeSet attrs)193 public CardStreamLinearLayout(Context context, AttributeSet attrs) { 194 super(context, attrs); 195 initialize(attrs, 0); 196 } 197 198 @SuppressLint("NewApi") CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle)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 */ addCard(View cardView, boolean canDismiss)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 addView(View child, int index, ViewGroup.LayoutParams params)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 onLayout(boolean changed, int l, int t, int r, int b)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 */ isSwiping(float deltaX, float deltaY)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) swipeView(View child, float deltaX, float deltaY)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 notifyOnDismissEvent( View child )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 */ getFirstVisibleCardTag()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 */ setFirstVisibleCard(String tag)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 */ triggerShowInitialAnimation()363 public void triggerShowInitialAnimation(){ 364 mShowInitialAnimation = true; 365 } 366 367 @TargetApi(Build.VERSION_CODES.HONEYCOMB) setCardStreamAnimator( CardStreamAnimator animators )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 */ setOnDismissListener(OnDissmissListener listener)390 public void setOnDismissListener(OnDissmissListener listener) { 391 mDismissListener = listener; 392 } 393 394 @TargetApi(Build.VERSION_CODES.HONEYCOMB) initialize(AttributeSet attrs, int defStyle)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 initCard(View cardView, boolean canDismiss)438 private void initCard(View cardView, boolean canDismiss) { 439 resetAnimatedView(cardView); 440 cardView.setOnTouchListener(mTouchListener); 441 if (!canDismiss) 442 mFixedViewList.add(cardView); 443 } 444 isFixedView(View v)445 private boolean isFixedView(View v) { 446 return mFixedViewList.contains(v); 447 } 448 resetAnimatedView(View child)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) runInitialAnimations()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 runShowActionAreaAnimation(View parent, View area)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 handleViewSwipingOut(final View child, float deltaX, float deltaY)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 handleViewSwipingIn(final View child, float deltaX, float deltaY)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 scrollToCard(String tag)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 { onDismiss(String tag)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 onAnimationStart(Animator animation)557 public void onAnimationStart(Animator animation) { 558 } 559 560 @Override onAnimationCancel(Animator animation)561 public void onAnimationCancel(Animator animation) { 562 } 563 564 @Override onAnimationRepeat(Animator animation)565 public void onAnimationRepeat(Animator animation) { 566 } 567 }//end of inner class 568 } 569