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; 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 42 import java.util.ArrayList; 43 44 /** 45 * A Layout that contains a stream of card views. 46 */ 47 public class CardStreamLinearLayout extends LinearLayout { 48 49 public static final int ANIMATION_SPEED_SLOW = 1001; 50 public static final int ANIMATION_SPEED_NORMAL = 1002; 51 public static final int ANIMATION_SPEED_FAST = 1003; 52 53 private static final String TAG = "CardStreamLinearLayout"; 54 private final ArrayList<View> mFixedViewList = new ArrayList<View>(); 55 private final Rect mChildRect = new Rect(); 56 private CardStreamAnimator mAnimators; 57 private OnDissmissListener mDismissListener = null; 58 private boolean mLayouted = false; 59 private boolean mSwiping = false; 60 private String mFirstVisibleCardTag = null; 61 private boolean mShowInitialAnimation = false; 62 63 /** 64 * Handle touch events to fade/move dragged items as they are swiped out 65 */ 66 private OnTouchListener mTouchListener = new OnTouchListener() { 67 68 private float mDownX; 69 private float mDownY; 70 71 @Override 72 public boolean onTouch(final View v, MotionEvent event) { 73 74 switch (event.getAction()) { 75 case MotionEvent.ACTION_DOWN: 76 mDownX = event.getX(); 77 mDownY = event.getY(); 78 break; 79 case MotionEvent.ACTION_CANCEL: 80 resetAnimatedView(v); 81 mSwiping = false; 82 mDownX = 0.f; 83 mDownY = 0.f; 84 break; 85 case MotionEvent.ACTION_MOVE: { 86 87 float x = event.getX() + v.getTranslationX(); 88 float y = event.getY() + v.getTranslationY(); 89 90 mDownX = mDownX == 0.f ? x : mDownX; 91 mDownY = mDownY == 0.f ? x : mDownY; 92 93 float deltaX = x - mDownX; 94 float deltaY = y - mDownY; 95 96 if (!mSwiping && isSwiping(deltaX, deltaY)) { 97 mSwiping = true; 98 v.getParent().requestDisallowInterceptTouchEvent(true); 99 } else { 100 swipeView(v, deltaX, deltaY); 101 } 102 } 103 break; 104 case MotionEvent.ACTION_UP: { 105 // User let go - figure out whether to animate the view out, or back into place 106 if (mSwiping) { 107 float x = event.getX() + v.getTranslationX(); 108 float y = event.getY() + v.getTranslationY(); 109 110 float deltaX = x - mDownX; 111 float deltaY = y - mDownX; 112 float deltaXAbs = Math.abs(deltaX); 113 114 // User let go - figure out whether to animate the view out, or back into place 115 boolean remove = deltaXAbs > v.getWidth() / 4 && !isFixedView(v); 116 if( remove ) 117 handleViewSwipingOut(v, deltaX, deltaY); 118 else 119 handleViewSwipingIn(v, deltaX, deltaY); 120 } 121 mDownX = 0.f; 122 mDownY = 0.f; 123 mSwiping = false; 124 } 125 break; 126 default: 127 return false; 128 } 129 return false; 130 } 131 }; 132 private int mSwipeSlop = -1; 133 /** 134 * Handle end-transition animation event of each child and launch a following animation. 135 */ 136 private LayoutTransition.TransitionListener mTransitionListener 137 = new LayoutTransition.TransitionListener() { 138 139 @Override 140 public void startTransition(LayoutTransition transition, ViewGroup container, View 141 view, int transitionType) { 142 Log.d(TAG, "Start LayoutTransition animation:" + transitionType); 143 } 144 145 @Override 146 public void endTransition(LayoutTransition transition, ViewGroup container, 147 final View view, int transitionType) { 148 149 Log.d(TAG, "End LayoutTransition animation:" + transitionType); 150 if (transitionType == LayoutTransition.APPEARING) { 151 final View area = view.findViewById(R.id.card_actionarea); 152 if (area != null) { 153 runShowActionAreaAnimation(container, area); 154 } 155 } 156 } 157 }; 158 /** 159 * Handle a hierarchy change event 160 * when a new child is added, scroll to bottom and hide action area.. 161 */ 162 private OnHierarchyChangeListener mOnHierarchyChangeListener 163 = new OnHierarchyChangeListener() { 164 @Override 165 public void onChildViewAdded(final View parent, final View child) { 166 167 Log.d(TAG, "child is added: " + child); 168 169 ViewParent scrollView = parent.getParent(); 170 if (scrollView != null && scrollView instanceof ScrollView) { 171 ((ScrollView) scrollView).fullScroll(FOCUS_DOWN); 172 } 173 174 if (getLayoutTransition() != null) { 175 View view = child.findViewById(R.id.card_actionarea); 176 if (view != null) 177 view.setAlpha(0.f); 178 } 179 } 180 181 @Override 182 public void onChildViewRemoved(View parent, View child) { 183 Log.d(TAG, "child is removed: " + child); 184 mFixedViewList.remove(child); 185 } 186 }; 187 private int mLastDownX; 188 CardStreamLinearLayout(Context context)189 public CardStreamLinearLayout(Context context) { 190 super(context); 191 initialize(null, 0); 192 } 193 CardStreamLinearLayout(Context context, AttributeSet attrs)194 public CardStreamLinearLayout(Context context, AttributeSet attrs) { 195 super(context, attrs); 196 initialize(attrs, 0); 197 } 198 199 @SuppressLint("NewApi") CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle)200 public CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle) { 201 super(context, attrs, defStyle); 202 initialize(attrs, defStyle); 203 } 204 205 /** 206 * add a card view w/ canDismiss flag. 207 * 208 * @param cardView a card view 209 * @param canDismiss flag to indicate this card is dismissible or not. 210 */ addCard(View cardView, boolean canDismiss)211 public void addCard(View cardView, boolean canDismiss) { 212 if (cardView.getParent() == null) { 213 initCard(cardView, canDismiss); 214 215 ViewGroup.LayoutParams param = cardView.getLayoutParams(); 216 if(param == null) 217 param = generateDefaultLayoutParams(); 218 219 super.addView(cardView, -1, param); 220 } 221 } 222 223 @Override addView(View child, int index, ViewGroup.LayoutParams params)224 public void addView(View child, int index, ViewGroup.LayoutParams params) { 225 if (child.getParent() == null) { 226 initCard(child, true); 227 super.addView(child, index, params); 228 } 229 } 230 231 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 232 @Override onLayout(boolean changed, int l, int t, int r, int b)233 protected void onLayout(boolean changed, int l, int t, int r, int b) { 234 super.onLayout(changed, l, t, r, b); 235 Log.d(TAG, "onLayout: " + changed); 236 237 if( changed && !mLayouted ){ 238 mLayouted = true; 239 240 ObjectAnimator animator; 241 LayoutTransition layoutTransition = new LayoutTransition(); 242 243 animator = mAnimators.getDisappearingAnimator(getContext()); 244 layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animator); 245 246 animator = mAnimators.getAppearingAnimator(getContext()); 247 layoutTransition.setAnimator(LayoutTransition.APPEARING, animator); 248 249 layoutTransition.addTransitionListener(mTransitionListener); 250 251 if( animator != null ) 252 layoutTransition.setDuration(animator.getDuration()); 253 254 setLayoutTransition(layoutTransition); 255 256 if( mShowInitialAnimation ) 257 runInitialAnimations(); 258 259 if (mFirstVisibleCardTag != null) { 260 scrollToCard(mFirstVisibleCardTag); 261 mFirstVisibleCardTag = null; 262 } 263 } 264 } 265 266 /** 267 * Check whether a user moved enough distance to start a swipe action or not. 268 * 269 * @param deltaX 270 * @param deltaY 271 * @return true if a user is swiping. 272 */ isSwiping(float deltaX, float deltaY)273 protected boolean isSwiping(float deltaX, float deltaY) { 274 275 if (mSwipeSlop < 0) { 276 //get swipping slop from ViewConfiguration; 277 mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 278 } 279 280 boolean swipping = false; 281 float absDeltaX = Math.abs(deltaX); 282 283 if( absDeltaX > mSwipeSlop ) 284 return true; 285 286 return swipping; 287 } 288 289 /** 290 * Swipe a view by moving distance 291 * 292 * @param child a target view 293 * @param deltaX x moving distance by x-axis. 294 * @param deltaY y moving distance by y-axis. 295 */ 296 @TargetApi(Build.VERSION_CODES.HONEYCOMB) swipeView(View child, float deltaX, float deltaY)297 protected void swipeView(View child, float deltaX, float deltaY) { 298 if (isFixedView(child)){ 299 deltaX = deltaX / 4; 300 } 301 302 float deltaXAbs = Math.abs(deltaX); 303 float fractionCovered = deltaXAbs / (float) child.getWidth(); 304 305 child.setTranslationX(deltaX); 306 child.setAlpha(1.f - fractionCovered); 307 308 if (deltaX > 0) 309 child.setRotationY(-15.f * fractionCovered); 310 else 311 child.setRotationY(15.f * fractionCovered); 312 } 313 notifyOnDismissEvent( View child )314 protected void notifyOnDismissEvent( View child ){ 315 if( child == null || mDismissListener == null ) 316 return; 317 318 mDismissListener.onDismiss((String) child.getTag()); 319 } 320 321 /** 322 * get the tag of the first visible child in this layout 323 * 324 * @return tag of the first visible child or null 325 */ getFirstVisibleCardTag()326 public String getFirstVisibleCardTag() { 327 328 final int count = getChildCount(); 329 330 if (count == 0) 331 return null; 332 333 for (int index = 0; index < count; ++index) { 334 //check the position of each view. 335 View child = getChildAt(index); 336 if (child.getGlobalVisibleRect(mChildRect) == true) 337 return (String) child.getTag(); 338 } 339 340 return null; 341 } 342 343 /** 344 * Set the first visible card of this linear layout. 345 * 346 * @param tag tag of a card which should already added to this layout. 347 */ setFirstVisibleCard(String tag)348 public void setFirstVisibleCard(String tag) { 349 if (tag == null) 350 return; //do nothing. 351 352 if (mLayouted) { 353 scrollToCard(tag); 354 } else { 355 //keep the tag for next use. 356 mFirstVisibleCardTag = tag; 357 } 358 } 359 360 /** 361 * If this flag is set, 362 * after finishing initial onLayout event, an initial animation which is defined in DefaultCardStreamAnimator is launched. 363 */ triggerShowInitialAnimation()364 public void triggerShowInitialAnimation(){ 365 mShowInitialAnimation = true; 366 } 367 368 @TargetApi(Build.VERSION_CODES.HONEYCOMB) setCardStreamAnimator( CardStreamAnimator animators )369 public void setCardStreamAnimator( CardStreamAnimator animators ){ 370 371 if( animators == null ) 372 mAnimators = new CardStreamAnimator.EmptyAnimator(); 373 else 374 mAnimators = animators; 375 376 LayoutTransition layoutTransition = getLayoutTransition(); 377 378 if( layoutTransition != null ){ 379 layoutTransition.setAnimator( LayoutTransition.APPEARING, 380 mAnimators.getAppearingAnimator(getContext()) ); 381 layoutTransition.setAnimator( LayoutTransition.DISAPPEARING, 382 mAnimators.getDisappearingAnimator(getContext()) ); 383 } 384 } 385 386 /** 387 * set a OnDismissListener which called when user dismiss a card. 388 * 389 * @param listener 390 */ setOnDismissListener(OnDissmissListener listener)391 public void setOnDismissListener(OnDissmissListener listener) { 392 mDismissListener = listener; 393 } 394 395 @TargetApi(Build.VERSION_CODES.HONEYCOMB) initialize(AttributeSet attrs, int defStyle)396 private void initialize(AttributeSet attrs, int defStyle) { 397 398 float speedFactor = 1.f; 399 400 if (attrs != null) { 401 TypedArray a = getContext().obtainStyledAttributes(attrs, 402 R.styleable.CardStream, defStyle, 0); 403 404 if( a != null ){ 405 int speedType = a.getInt(R.styleable.CardStream_animationDuration, 1001); 406 switch (speedType){ 407 case ANIMATION_SPEED_FAST: 408 speedFactor = 0.5f; 409 break; 410 case ANIMATION_SPEED_NORMAL: 411 speedFactor = 1.f; 412 break; 413 case ANIMATION_SPEED_SLOW: 414 speedFactor = 2.f; 415 break; 416 } 417 418 String animatorName = a.getString(R.styleable.CardStream_animators); 419 420 try { 421 if( animatorName != null ) 422 mAnimators = (CardStreamAnimator) getClass().getClassLoader() 423 .loadClass(animatorName).newInstance(); 424 } catch (Exception e) { 425 Log.e(TAG, "Fail to load animator:" + animatorName, e); 426 } finally { 427 if(mAnimators == null) 428 mAnimators = new DefaultCardStreamAnimator(); 429 } 430 a.recycle(); 431 } 432 } 433 434 mAnimators.setSpeedFactor(speedFactor); 435 mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 436 setOnHierarchyChangeListener(mOnHierarchyChangeListener); 437 } 438 initCard(View cardView, boolean canDismiss)439 private void initCard(View cardView, boolean canDismiss) { 440 resetAnimatedView(cardView); 441 cardView.setOnTouchListener(mTouchListener); 442 if (!canDismiss) 443 mFixedViewList.add(cardView); 444 } 445 isFixedView(View v)446 private boolean isFixedView(View v) { 447 return mFixedViewList.contains(v); 448 } 449 resetAnimatedView(View child)450 private void resetAnimatedView(View child) { 451 child.setAlpha(1.f); 452 child.setTranslationX(0.f); 453 child.setTranslationY(0.f); 454 child.setRotation(0.f); 455 child.setRotationY(0.f); 456 child.setRotationX(0.f); 457 child.setScaleX(1.f); 458 child.setScaleY(1.f); 459 } 460 461 @TargetApi(Build.VERSION_CODES.HONEYCOMB) runInitialAnimations()462 private void runInitialAnimations() { 463 if( mAnimators == null ) 464 return; 465 466 final int count = getChildCount(); 467 468 for (int index = 0; index < count; ++index) { 469 final View child = getChildAt(index); 470 ObjectAnimator animator = mAnimators.getInitalAnimator(getContext()); 471 if( animator != null ){ 472 animator.setTarget(child); 473 animator.start(); 474 } 475 } 476 } 477 runShowActionAreaAnimation(View parent, View area)478 private void runShowActionAreaAnimation(View parent, View area) { 479 area.setPivotY(0.f); 480 area.setPivotX(parent.getWidth() / 2.f); 481 482 area.setAlpha(0.5f); 483 area.setRotationX(-90.f); 484 area.animate().rotationX(0.f).alpha(1.f).setDuration(400); 485 } 486 handleViewSwipingOut(final View child, float deltaX, float deltaY)487 private void handleViewSwipingOut(final View child, float deltaX, float deltaY) { 488 ObjectAnimator animator = mAnimators.getSwipeOutAnimator(child, deltaX, deltaY); 489 if( animator != null ){ 490 animator.addListener(new EndAnimationWrapper() { 491 @Override 492 public void onAnimationEnd(Animator animation) { 493 removeView(child); 494 notifyOnDismissEvent(child); 495 } 496 }); 497 } else { 498 removeView(child); 499 notifyOnDismissEvent(child); 500 } 501 502 if( animator != null ){ 503 animator.setTarget(child); 504 animator.start(); 505 } 506 } 507 handleViewSwipingIn(final View child, float deltaX, float deltaY)508 private void handleViewSwipingIn(final View child, float deltaX, float deltaY) { 509 ObjectAnimator animator = mAnimators.getSwipeInAnimator(child, deltaX, deltaY); 510 if( animator != null ){ 511 animator.addListener(new EndAnimationWrapper() { 512 @Override 513 public void onAnimationEnd(Animator animation) { 514 child.setTranslationY(0.f); 515 child.setTranslationX(0.f); 516 } 517 }); 518 } else { 519 child.setTranslationY(0.f); 520 child.setTranslationX(0.f); 521 } 522 523 if( animator != null ){ 524 animator.setTarget(child); 525 animator.start(); 526 } 527 } 528 scrollToCard(String tag)529 private void scrollToCard(String tag) { 530 531 532 final int count = getChildCount(); 533 for (int index = 0; index < count; ++index) { 534 View child = getChildAt(index); 535 536 if (tag.equals(child.getTag())) { 537 538 ViewParent parent = getParent(); 539 if( parent != null && parent instanceof ScrollView ){ 540 ((ScrollView)parent).smoothScrollTo( 541 0, child.getTop() - getPaddingTop() - child.getPaddingTop()); 542 } 543 return; 544 } 545 } 546 } 547 548 public interface OnDissmissListener { onDismiss(String tag)549 public void onDismiss(String tag); 550 } 551 552 /** 553 * Empty default AnimationListener 554 */ 555 private abstract class EndAnimationWrapper implements Animator.AnimatorListener { 556 557 @Override onAnimationStart(Animator animation)558 public void onAnimationStart(Animator animation) { 559 } 560 561 @Override onAnimationCancel(Animator animation)562 public void onAnimationCancel(Animator animation) { 563 } 564 565 @Override onAnimationRepeat(Animator animation)566 public void onAnimationRepeat(Animator animation) { 567 } 568 }//end of inner class 569 } 570