package com.asha;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ScrollingView;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
import android.widget.ScrollView;
import com.asha.library.R;
import java.util.LinkedList;
import java.util.List;
/**
* Created by hzqiujiadi on 15/11/20.
* hzqiujiadi ashqalcn@gmail.com
*/
public class ChromeLikeSwipeLayout extends ViewGroup implements TouchManager.ITouchCallback, NestedScrollingParent {
private static final String TAG = "ChromeLikeSwipeLayout";
private View mTarget; // the target of the gesture
private ChromeLikeLayout mChromeLikeLayout;
private int mCollapseDuration = 300;
private boolean mAnimationStarted;
private final NestedScrollingChildHelper mScrollingChildHelper;
private final NestedScrollingParentHelper mScrollingParentHelper;
private final StatusManager mStatusManager = new StatusManager();
private final TouchManager mTouchManager = new TouchManager(this);
private IOnItemSelectedListener mOnItemSelectedListener;
private LinkedList<IOnExpandViewListener> mExpandListeners = new LinkedList<>();
private boolean mEnabled = true;
public ChromeLikeSwipeLayout(Context context) {
this(context, null);
}
public ChromeLikeSwipeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ChromeLikeSwipeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
Config config = makeConfig();
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ChromeLikeSwipeLayout,defStyleAttr,0);
if ( ta != null ){
if (ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_circleColor))
config.circleColor(ta.getColor(R.styleable.ChromeLikeSwipeLayout_clwl_circleColor,Config.DEFAULT));
if ( ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_gap))
config.gap(ta.getDimensionPixelOffset(R.styleable.ChromeLikeSwipeLayout_clwl_gap,Config.DEFAULT));
if ( ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_radius))
config.radius(ta.getDimensionPixelOffset(R.styleable.ChromeLikeSwipeLayout_clwl_radius,Config.DEFAULT));
if ( ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_collapseDuration))
config.collapseDuration(ta.getInt(R.styleable.ChromeLikeSwipeLayout_clwl_collapseDuration,Config.DEFAULT));
if ( ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_rippleDuration))
config.rippleDuration(ta.getInt(R.styleable.ChromeLikeSwipeLayout_clwl_rippleDuration,Config.DEFAULT));
if ( ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_gummyDuration))
config.gummyDuration(ta.getInt(R.styleable.ChromeLikeSwipeLayout_clwl_gummyDuration,Config.DEFAULT));
if ( ta.hasValue(R.styleable.ChromeLikeSwipeLayout_clwl_maxHeight))
config.setMaxHeight(ta.getDimensionPixelOffset(R.styleable.ChromeLikeSwipeLayout_clwl_maxHeight,Config.DEFAULT));
ta.recycle();
}
config.setTo(this);
// init NestedScrollingChildHelper
mScrollingChildHelper = new NestedScrollingChildHelper(this);
mScrollingParentHelper = new NestedScrollingParentHelper(this);
setNestedScrollingEnabled(true);
}
private void init() {
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchManager.setTouchSlop((int) (configuration.getScaledTouchSlop() * 1.1f));
mChromeLikeLayout = new ChromeLikeLayout(getContext());
mChromeLikeLayout.setRippleListener(new ChromeLikeLayout.IOnRippleListener() {
@Override
public void onRippleAnimFinished(int index) {
mStatusManager.toRestore();
if ( !mAnimationStarted ) launchResetAnim();
mTouchManager.endDrag();
if ( mOnItemSelectedListener != null )
mOnItemSelectedListener.onItemSelected(index);
}
});
addOnExpandViewListener(mChromeLikeLayout);
addView(mChromeLikeLayout);
}
@Override
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
//Log.d(TAG,"onInterceptTouchEvent:" + event);
if (!mEnabled) return false;
if ( mAnimationStarted ) return false;
if ( canChildDragDown(mTouchManager.event2Point(event)) ) return false;
return mTouchManager.onFeedInterceptEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mTouchManager.onFeedTouchEvent(event);
}
private void childOffsetTopAndBottom(int target){
mTarget.offsetTopAndBottom( target );
mChromeLikeLayout.offsetTopAndBottom( target );
requestLayout();
}
// only execute on ACTION_UP
private void executeAction(boolean isExpanded) {
if ( isExpanded ){
mStatusManager.toBusy();
} else {
if ( mStatusManager.isBusying() ) return;
if ( mAnimationStarted ) return;
launchResetAnim();
mTouchManager.endDrag();
}
}
private void launchResetAnim(){
boolean isFromCancel = !mStatusManager.isRestoring();
launchResetAnim(isFromCancel);
}
private void launchResetAnim( final boolean isFromCancel ){
ensureTarget();
final int from = mTarget.getTop();
final int to = 0;
Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float step = (to - from) * interpolatedTime + from;
int top = mTarget.getTop();
notifyOnExpandListeners( mTouchManager.calExpandProgress(top) ,isFromCancel);
childOffsetTopAndBottom( mTouchManager.calTargetTopOffset(top, Math.round(step)) );
}
};
animation.setDuration(mCollapseDuration);
animation.setInterpolator(new DecelerateInterpolator());
animation.setAnimationListener(new AnimationListenerAdapter() {
@Override
public void onAnimationEnd(Animation animation) {
mAnimationStarted = false;
mStatusManager.toIdle();
}
});
this.clearAnimation();
this.startAnimation(animation);
mAnimationStarted = true;
}
@Override
public void addView(View child, int index, LayoutParams params) {
boolean touchAlwaysTrue = child instanceof ScrollView
|| child instanceof AbsListView
|| child instanceof ScrollingView
|| child instanceof TouchAlwaysTrueLayout
|| child instanceof ChromeLikeLayout;
if ( !touchAlwaysTrue ) child = TouchAlwaysTrueLayout.wrap(child);
super.addView(child,index,params);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
View child = mTarget;
int childLeft = getPaddingLeft();
int childTop = child.getTop();
int childWidth = width - getPaddingLeft() - getPaddingRight();
int childHeight = height - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
child = mChromeLikeLayout;
childLeft = getPaddingLeft();
childTop = mTarget.getTop() - child.getMeasuredHeight();
childWidth = width - getPaddingLeft() - getPaddingRight();
childHeight = child.getMeasuredHeight();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
mTarget.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
mChromeLikeLayout.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mTarget.getTop(), MeasureSpec.EXACTLY));
}
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// Nope.
super.requestDisallowInterceptTouchEvent(b);
}
private boolean canChildDragDown(PointF pointF){
ensureTarget();
if ( mTarget instanceof TouchAlwaysTrueLayout )
return ((TouchAlwaysTrueLayout) mTarget).canChildDragDown(pointF);
else return ViewCompat.canScrollVertically(mTarget,-1);
}
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid
// out yet.
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mChromeLikeLayout)) {
mTarget = child;
mChromeLikeLayout.bringToFront();
break;
}
}
}
}
private void setCollapseDuration(int collapseDuration) {
this.mCollapseDuration = collapseDuration;
}
private void setConfig(Config config){
if ( config.mIcons != null )
mChromeLikeLayout.setIcons(config.mIcons);
if ( config.mBackgroundResId != Config.DEFAULT )
mChromeLikeLayout.setBackgroundResource(config.mBackgroundResId);
if ( config.mBackgroundColor != Config.DEFAULT )
mChromeLikeLayout.setBackgroundColor(config.mBackgroundColor);
if ( config.mCircleColor != Config.DEFAULT )
mChromeLikeLayout.setCircleColor(config.mCircleColor);
if ( config.mRadius != Config.DEFAULT )
mChromeLikeLayout.setRadius(config.mRadius);
if ( config.mRadius != Config.DEFAULT )
mChromeLikeLayout.setGap(config.mGap);
if ( config.mRippleDuration != Config.DEFAULT )
mChromeLikeLayout.setRippleDuration(config.mRippleDuration);
if ( config.mGummyDuration != Config.DEFAULT )
mChromeLikeLayout.setGummyDuration(config.mGummyDuration);
if ( config.mCollapseDuration != Config.DEFAULT )
setCollapseDuration(config.mCollapseDuration);
if ( config.maxHeight != Config.DEFAULT )
mTouchManager.setMaxHeight(config.maxHeight);
mOnItemSelectedListener = config.mOnItemSelectedListener;
}
public void notifyOnExpandListeners(float fraction, boolean isFromCancel){
fraction = fraction < 1 ? fraction : 1;
for ( IOnExpandViewListener listener : mExpandListeners )
listener.onExpandView(fraction,isFromCancel);
}
public void addOnExpandViewListener(IOnExpandViewListener listener){
mExpandListeners.add(listener);
}
public void removeOnExpandViewListener(IOnExpandViewListener listener){
mExpandListeners.remove(listener);
}
public void removeAllOnExpandViewListener(){
mExpandListeners.clear();
}
@Override
public void onActionDown() {
mChromeLikeLayout.onActionDown();
}
@Override
public void onActionUp(boolean isExpanded) {
executeAction(isExpanded);
mChromeLikeLayout.onActionUpOrCancel(isExpanded);
}
@Override
public void onActionCancel(boolean isExpanded) {
mChromeLikeLayout.onActionUpOrCancel(isExpanded);
}
@Override
public void onActionMove(boolean isExpanded, TouchManager touchManager) {
mChromeLikeLayout.onActionMove(isExpanded, touchManager);
ensureTarget();
View child = mTarget;
int currentTop = child.getTop();
if ( mTouchManager.isBeginDragging() ) {
if ( !isExpanded )
notifyOnExpandListeners(mTouchManager.calExpandProgress(currentTop), true);
childOffsetTopAndBottom(mTouchManager.calTargetTopOffset(currentTop));
}
}
@Override
public void onBeginDragging() {
mStatusManager.toChanged();
}
public interface IOnExpandViewListener {
void onExpandView(float fraction, boolean isFromCancel);
}
public static Config makeConfig(){
return new Config();
}
/***
* Builder
* */
public static class Config{
private List<Integer> mIcons;
private IOnItemSelectedListener mOnItemSelectedListener;
private int mCircleColor = DEFAULT;
private int mBackgroundResId = DEFAULT;
private int mBackgroundColor = DEFAULT;
private int mRadius = DEFAULT;
private int mGap = DEFAULT;
private int mCollapseDuration = DEFAULT;
private int mRippleDuration = DEFAULT;
private int mGummyDuration = DEFAULT;
private int maxHeight = DEFAULT;
private static final int DEFAULT = -1;
private Config(){
}
public Config addIcon(@DrawableRes int drawableResId){
if ( mIcons == null ) mIcons = new LinkedList<>();
mIcons.add(drawableResId);
return this;
}
public Config background(@DrawableRes int backgroundResId){
this.mBackgroundResId = backgroundResId;
return this;
}
public Config backgroundColor(@ColorInt int color){
this.mBackgroundColor = color;
return this;
}
public Config circleColor(@ColorInt int color){
this.mCircleColor = color;
return this;
}
public Config listenItemSelected(IOnItemSelectedListener listener){
this.mOnItemSelectedListener = listener;
return this;
}
public Config radius(int radius){
this.mRadius = radius;
return this;
}
public Config gap(int gap){
this.mGap = gap;
return this;
}
public Config collapseDuration(int duration){
this.mCollapseDuration = duration;
return this;
}
public Config rippleDuration(int duration){
this.mRippleDuration = duration;
return this;
}
public Config gummyDuration(int duration){
this.mGummyDuration = duration;
return this;
}
public Config setMaxHeight(int maxHeight) {
this.maxHeight = maxHeight;
return this;
}
public void setTo(ChromeLikeSwipeLayout chromeLikeSwipeLayout){
chromeLikeSwipeLayout.setConfig(this);
}
}
public static int dp2px(float valueInDp) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (valueInDp * scale + 0.5f);
}
public interface IOnItemSelectedListener{
void onItemSelected(int index);
}
/****
* Response for managing dropdown status
* */
public static class StatusManager {
private int mStatus = STATUS_IDLE;
private static final int STATUS_IDLE = 0;
private static final int STATUS_CHANGED = 1;
private static final int STATUS_BUSY = 2;
private static final int STATUS_RESTORE = 3;
public void toIdle(){
mStatus = STATUS_IDLE;
}
public void toBusy(){
mStatus = STATUS_BUSY;
}
public void toRestore(){
mStatus = STATUS_RESTORE;
}
public void toChanged(){
mStatus = STATUS_CHANGED;
}
public boolean isChanged(){
return mStatus == STATUS_CHANGED;
}
public boolean isBusying(){
return mStatus == STATUS_BUSY;
}
public boolean isRestoring(){
return mStatus == STATUS_RESTORE;
}
public boolean isIdle(){
return mStatus == STATUS_IDLE;
}
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
boolean result = mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
return result;
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
boolean result = mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
return result;
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
// mScrollingParentHelper
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
}
@Override
public int getNestedScrollAxes() {
return mScrollingParentHelper.getNestedScrollAxes();
}
@Override
public void onStopNestedScroll(View target) {
mScrollingParentHelper.onStopNestedScroll(target);
}
// do nothing now
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean result = this.startNestedScroll(nestedScrollAxes);
if ( result ) mTouchManager.setInterceptEnabled(false);
return true;
}
int[] offsets = new int[2];
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
boolean result = this.dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsets);
if ( result ){
boolean consumed = (offsets[1] + dyUnconsumed) == 0 && dyUnconsumed != 0;
mTouchManager.setInterceptEnabled( !consumed );
}
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
this.dispatchNestedPreScroll(dx,dy,consumed,offsets);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
}