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 } 568