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 }