| /* |
| * Copyright 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| package com.example.android.batchstepsensor.cardstream; |
| |
| import android.animation.Animator; |
| import android.animation.LayoutTransition; |
| import android.animation.ObjectAnimator; |
| import android.annotation.SuppressLint; |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Rect; |
| import android.os.Build; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import android.widget.LinearLayout; |
| import android.widget.ScrollView; |
| |
| import com.example.android.common.logger.Log; |
| import com.example.android.batchstepsensor.R; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * A Layout that contains a stream of card views. |
| */ |
| public class CardStreamLinearLayout extends LinearLayout { |
| |
| public static final int ANIMATION_SPEED_SLOW = 1001; |
| public static final int ANIMATION_SPEED_NORMAL = 1002; |
| public static final int ANIMATION_SPEED_FAST = 1003; |
| |
| private static final String TAG = "CardStreamLinearLayout"; |
| private final ArrayList<View> mFixedViewList = new ArrayList<View>(); |
| private final Rect mChildRect = new Rect(); |
| private CardStreamAnimator mAnimators; |
| private OnDissmissListener mDismissListener = null; |
| private boolean mLayouted = false; |
| private boolean mSwiping = false; |
| private String mFirstVisibleCardTag = null; |
| private boolean mShowInitialAnimation = false; |
| |
| /** |
| * Handle touch events to fade/move dragged items as they are swiped out |
| */ |
| private OnTouchListener mTouchListener = new OnTouchListener() { |
| |
| private float mDownX; |
| private float mDownY; |
| |
| @Override |
| public boolean onTouch(final View v, MotionEvent event) { |
| |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| mDownX = event.getX(); |
| mDownY = event.getY(); |
| break; |
| case MotionEvent.ACTION_CANCEL: |
| resetAnimatedView(v); |
| mSwiping = false; |
| mDownX = 0.f; |
| mDownY = 0.f; |
| break; |
| case MotionEvent.ACTION_MOVE: { |
| |
| float x = event.getX() + v.getTranslationX(); |
| float y = event.getY() + v.getTranslationY(); |
| |
| mDownX = mDownX == 0.f ? x : mDownX; |
| mDownY = mDownY == 0.f ? x : mDownY; |
| |
| float deltaX = x - mDownX; |
| float deltaY = y - mDownY; |
| |
| if (!mSwiping && isSwiping(deltaX, deltaY)) { |
| mSwiping = true; |
| v.getParent().requestDisallowInterceptTouchEvent(true); |
| } else { |
| swipeView(v, deltaX, deltaY); |
| } |
| } |
| break; |
| case MotionEvent.ACTION_UP: { |
| // User let go - figure out whether to animate the view out, or back into place |
| if (mSwiping) { |
| float x = event.getX() + v.getTranslationX(); |
| float y = event.getY() + v.getTranslationY(); |
| |
| float deltaX = x - mDownX; |
| float deltaY = y - mDownX; |
| float deltaXAbs = Math.abs(deltaX); |
| |
| // User let go - figure out whether to animate the view out, or back into place |
| boolean remove = deltaXAbs > v.getWidth() / 4 && !isFixedView(v); |
| if( remove ) |
| handleViewSwipingOut(v, deltaX, deltaY); |
| else |
| handleViewSwipingIn(v, deltaX, deltaY); |
| } |
| mDownX = 0.f; |
| mDownY = 0.f; |
| mSwiping = false; |
| } |
| break; |
| default: |
| return false; |
| } |
| return false; |
| } |
| }; |
| private int mSwipeSlop = -1; |
| /** |
| * Handle end-transition animation event of each child and launch a following animation. |
| */ |
| private LayoutTransition.TransitionListener mTransitionListener |
| = new LayoutTransition.TransitionListener() { |
| |
| @Override |
| public void startTransition(LayoutTransition transition, ViewGroup container, View |
| view, int transitionType) { |
| Log.d(TAG, "Start LayoutTransition animation:" + transitionType); |
| } |
| |
| @Override |
| public void endTransition(LayoutTransition transition, ViewGroup container, |
| final View view, int transitionType) { |
| |
| Log.d(TAG, "End LayoutTransition animation:" + transitionType); |
| if (transitionType == LayoutTransition.APPEARING) { |
| final View area = view.findViewById(R.id.card_actionarea); |
| if (area != null) { |
| runShowActionAreaAnimation(container, area); |
| } |
| } |
| } |
| }; |
| /** |
| * Handle a hierarchy change event |
| * when a new child is added, scroll to bottom and hide action area.. |
| */ |
| private OnHierarchyChangeListener mOnHierarchyChangeListener |
| = new OnHierarchyChangeListener() { |
| @Override |
| public void onChildViewAdded(final View parent, final View child) { |
| |
| Log.d(TAG, "child is added: " + child); |
| |
| ViewParent scrollView = parent.getParent(); |
| if (scrollView != null && scrollView instanceof ScrollView) { |
| ((ScrollView) scrollView).fullScroll(FOCUS_DOWN); |
| } |
| |
| if (getLayoutTransition() != null) { |
| View view = child.findViewById(R.id.card_actionarea); |
| if (view != null) |
| view.setAlpha(0.f); |
| } |
| } |
| |
| @Override |
| public void onChildViewRemoved(View parent, View child) { |
| Log.d(TAG, "child is removed: " + child); |
| mFixedViewList.remove(child); |
| } |
| }; |
| private int mLastDownX; |
| |
| public CardStreamLinearLayout(Context context) { |
| super(context); |
| initialize(null, 0); |
| } |
| |
| public CardStreamLinearLayout(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| initialize(attrs, 0); |
| } |
| |
| @SuppressLint("NewApi") |
| public CardStreamLinearLayout(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| initialize(attrs, defStyle); |
| } |
| |
| /** |
| * add a card view w/ canDismiss flag. |
| * |
| * @param cardView a card view |
| * @param canDismiss flag to indicate this card is dismissible or not. |
| */ |
| public void addCard(View cardView, boolean canDismiss) { |
| if (cardView.getParent() == null) { |
| initCard(cardView, canDismiss); |
| |
| ViewGroup.LayoutParams param = cardView.getLayoutParams(); |
| if(param == null) |
| param = generateDefaultLayoutParams(); |
| |
| super.addView(cardView, -1, param); |
| } |
| } |
| |
| @Override |
| public void addView(View child, int index, ViewGroup.LayoutParams params) { |
| if (child.getParent() == null) { |
| initCard(child, true); |
| super.addView(child, index, params); |
| } |
| } |
| |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| super.onLayout(changed, l, t, r, b); |
| Log.d(TAG, "onLayout: " + changed); |
| |
| if( changed && !mLayouted ){ |
| mLayouted = true; |
| |
| ObjectAnimator animator; |
| LayoutTransition layoutTransition = new LayoutTransition(); |
| |
| animator = mAnimators.getDisappearingAnimator(getContext()); |
| layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animator); |
| |
| animator = mAnimators.getAppearingAnimator(getContext()); |
| layoutTransition.setAnimator(LayoutTransition.APPEARING, animator); |
| |
| layoutTransition.addTransitionListener(mTransitionListener); |
| |
| if( animator != null ) |
| layoutTransition.setDuration(animator.getDuration()); |
| |
| setLayoutTransition(layoutTransition); |
| |
| if( mShowInitialAnimation ) |
| runInitialAnimations(); |
| |
| if (mFirstVisibleCardTag != null) { |
| scrollToCard(mFirstVisibleCardTag); |
| mFirstVisibleCardTag = null; |
| } |
| } |
| } |
| |
| /** |
| * Check whether a user moved enough distance to start a swipe action or not. |
| * |
| * @param deltaX |
| * @param deltaY |
| * @return true if a user is swiping. |
| */ |
| protected boolean isSwiping(float deltaX, float deltaY) { |
| |
| if (mSwipeSlop < 0) { |
| //get swipping slop from ViewConfiguration; |
| mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| } |
| |
| boolean swipping = false; |
| float absDeltaX = Math.abs(deltaX); |
| |
| if( absDeltaX > mSwipeSlop ) |
| return true; |
| |
| return swipping; |
| } |
| |
| /** |
| * Swipe a view by moving distance |
| * |
| * @param child a target view |
| * @param deltaX x moving distance by x-axis. |
| * @param deltaY y moving distance by y-axis. |
| */ |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| protected void swipeView(View child, float deltaX, float deltaY) { |
| if (isFixedView(child)){ |
| deltaX = deltaX / 4; |
| } |
| |
| float deltaXAbs = Math.abs(deltaX); |
| float fractionCovered = deltaXAbs / (float) child.getWidth(); |
| |
| child.setTranslationX(deltaX); |
| child.setAlpha(1.f - fractionCovered); |
| |
| if (deltaX > 0) |
| child.setRotationY(-15.f * fractionCovered); |
| else |
| child.setRotationY(15.f * fractionCovered); |
| } |
| |
| protected void notifyOnDismissEvent( View child ){ |
| if( child == null || mDismissListener == null ) |
| return; |
| |
| mDismissListener.onDismiss((String) child.getTag()); |
| } |
| |
| /** |
| * get the tag of the first visible child in this layout |
| * |
| * @return tag of the first visible child or null |
| */ |
| public String getFirstVisibleCardTag() { |
| |
| final int count = getChildCount(); |
| |
| if (count == 0) |
| return null; |
| |
| for (int index = 0; index < count; ++index) { |
| //check the position of each view. |
| View child = getChildAt(index); |
| if (child.getGlobalVisibleRect(mChildRect) == true) |
| return (String) child.getTag(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Set the first visible card of this linear layout. |
| * |
| * @param tag tag of a card which should already added to this layout. |
| */ |
| public void setFirstVisibleCard(String tag) { |
| if (tag == null) |
| return; //do nothing. |
| |
| if (mLayouted) { |
| scrollToCard(tag); |
| } else { |
| //keep the tag for next use. |
| mFirstVisibleCardTag = tag; |
| } |
| } |
| |
| /** |
| * If this flag is set, |
| * after finishing initial onLayout event, an initial animation which is defined in DefaultCardStreamAnimator is launched. |
| */ |
| public void triggerShowInitialAnimation(){ |
| mShowInitialAnimation = true; |
| } |
| |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| public void setCardStreamAnimator( CardStreamAnimator animators ){ |
| |
| if( animators == null ) |
| mAnimators = new CardStreamAnimator.EmptyAnimator(); |
| else |
| mAnimators = animators; |
| |
| LayoutTransition layoutTransition = getLayoutTransition(); |
| |
| if( layoutTransition != null ){ |
| layoutTransition.setAnimator( LayoutTransition.APPEARING, |
| mAnimators.getAppearingAnimator(getContext()) ); |
| layoutTransition.setAnimator( LayoutTransition.DISAPPEARING, |
| mAnimators.getDisappearingAnimator(getContext()) ); |
| } |
| } |
| |
| /** |
| * set a OnDismissListener which called when user dismiss a card. |
| * |
| * @param listener |
| */ |
| public void setOnDismissListener(OnDissmissListener listener) { |
| mDismissListener = listener; |
| } |
| |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| private void initialize(AttributeSet attrs, int defStyle) { |
| |
| float speedFactor = 1.f; |
| |
| if (attrs != null) { |
| TypedArray a = getContext().obtainStyledAttributes(attrs, |
| R.styleable.CardStream, defStyle, 0); |
| |
| if( a != null ){ |
| int speedType = a.getInt(R.styleable.CardStream_animationDuration, 1001); |
| switch (speedType){ |
| case ANIMATION_SPEED_FAST: |
| speedFactor = 0.5f; |
| break; |
| case ANIMATION_SPEED_NORMAL: |
| speedFactor = 1.f; |
| break; |
| case ANIMATION_SPEED_SLOW: |
| speedFactor = 2.f; |
| break; |
| } |
| |
| String animatorName = a.getString(R.styleable.CardStream_animators); |
| |
| try { |
| if( animatorName != null ) |
| mAnimators = (CardStreamAnimator) getClass().getClassLoader() |
| .loadClass(animatorName).newInstance(); |
| } catch (Exception e) { |
| Log.e(TAG, "Fail to load animator:" + animatorName, e); |
| } finally { |
| if(mAnimators == null) |
| mAnimators = new DefaultCardStreamAnimator(); |
| } |
| a.recycle(); |
| } |
| } |
| |
| mAnimators.setSpeedFactor(speedFactor); |
| mSwipeSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| setOnHierarchyChangeListener(mOnHierarchyChangeListener); |
| } |
| |
| private void initCard(View cardView, boolean canDismiss) { |
| resetAnimatedView(cardView); |
| cardView.setOnTouchListener(mTouchListener); |
| if (!canDismiss) |
| mFixedViewList.add(cardView); |
| } |
| |
| private boolean isFixedView(View v) { |
| return mFixedViewList.contains(v); |
| } |
| |
| private void resetAnimatedView(View child) { |
| child.setAlpha(1.f); |
| child.setTranslationX(0.f); |
| child.setTranslationY(0.f); |
| child.setRotation(0.f); |
| child.setRotationY(0.f); |
| child.setRotationX(0.f); |
| child.setScaleX(1.f); |
| child.setScaleY(1.f); |
| } |
| |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| private void runInitialAnimations() { |
| if( mAnimators == null ) |
| return; |
| |
| final int count = getChildCount(); |
| |
| for (int index = 0; index < count; ++index) { |
| final View child = getChildAt(index); |
| ObjectAnimator animator = mAnimators.getInitalAnimator(getContext()); |
| if( animator != null ){ |
| animator.setTarget(child); |
| animator.start(); |
| } |
| } |
| } |
| |
| private void runShowActionAreaAnimation(View parent, View area) { |
| area.setPivotY(0.f); |
| area.setPivotX(parent.getWidth() / 2.f); |
| |
| area.setAlpha(0.5f); |
| area.setRotationX(-90.f); |
| area.animate().rotationX(0.f).alpha(1.f).setDuration(400); |
| } |
| |
| private void handleViewSwipingOut(final View child, float deltaX, float deltaY) { |
| ObjectAnimator animator = mAnimators.getSwipeOutAnimator(child, deltaX, deltaY); |
| if( animator != null ){ |
| animator.addListener(new EndAnimationWrapper() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| removeView(child); |
| notifyOnDismissEvent(child); |
| } |
| }); |
| } else { |
| removeView(child); |
| notifyOnDismissEvent(child); |
| } |
| |
| if( animator != null ){ |
| animator.setTarget(child); |
| animator.start(); |
| } |
| } |
| |
| private void handleViewSwipingIn(final View child, float deltaX, float deltaY) { |
| ObjectAnimator animator = mAnimators.getSwipeInAnimator(child, deltaX, deltaY); |
| if( animator != null ){ |
| animator.addListener(new EndAnimationWrapper() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| child.setTranslationY(0.f); |
| child.setTranslationX(0.f); |
| } |
| }); |
| } else { |
| child.setTranslationY(0.f); |
| child.setTranslationX(0.f); |
| } |
| |
| if( animator != null ){ |
| animator.setTarget(child); |
| animator.start(); |
| } |
| } |
| |
| private void scrollToCard(String tag) { |
| |
| |
| final int count = getChildCount(); |
| for (int index = 0; index < count; ++index) { |
| View child = getChildAt(index); |
| |
| if (tag.equals(child.getTag())) { |
| |
| ViewParent parent = getParent(); |
| if( parent != null && parent instanceof ScrollView ){ |
| ((ScrollView)parent).smoothScrollTo( |
| 0, child.getTop() - getPaddingTop() - child.getPaddingTop()); |
| } |
| return; |
| } |
| } |
| } |
| |
| public interface OnDissmissListener { |
| public void onDismiss(String tag); |
| } |
| |
| /** |
| * Empty default AnimationListener |
| */ |
| private abstract class EndAnimationWrapper implements Animator.AnimatorListener { |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animation) { |
| } |
| }//end of inner class |
| } |