/*
* 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
}