package com.think.tlr;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import com.think.tlr.TLRLinearLayout.LoadStatus;
import com.think.tlr.TLRLinearLayout.RefreshStatus;
import java.util.ArrayList;
import java.util.List;
/**
* @author borney
* @date 4/28/17
*/
class TLRCalculator {
private static final boolean DEBUG = false;
private float mDownX, mDownY;
private float mLastX, mLastY;
/**
* Touch View 移动阀值
*/
private float mOffsetX, mOffsetY;
/**
* View 偏移坐标
*/
private int mTotalOffsetY, mTotalOffsetX;
/**
* 刷新阀值系数,加载阀值系数
*/
private float mRefreshThreshold = 1.0f, mLoadThreshold = 1.0f;
/**
* 阻尼系数
*/
private float mResistance = 1.6f;
/**
* 关闭和打开动画时间
*/
private int mCloseAnimDuration = 500, mOpenAnimDuration = 800;
private int mHeadHeight;
private int mFootHeight;
/**
* 刷新或加载移动的最大距离
*/
private int mRefreshMaxMoveDistance = -1, mLoadMaxMoveDistance = -1;
/**
* 是否保存刷新view/加载view
*/
private boolean isKeepHeadRefreshing = true, isKeepFootLoading = true;
/**
* UI是否回归到初始状态
*/
private boolean isBackStatus = true;
private TLRStatusController mStatusController;
private TLRUIHandler mTLRUiHandler;
private ValueAnimator mAutoAnimator, mResetAnimator, mKeepAnimator;
private final List<TLRUIHandlerHook> mHooks = new ArrayList<>();
/**
* 刷新阀值(高度), 加载阀值
*/
int refreshThresholdHeight, loadThresholdHeight;
TLRLinearLayout tLRLinearLayout;
private int mTouchSlop;
private Direction mDirection = Direction.NONE;
public TLRCalculator(TLRLinearLayout layout, AttributeSet attrs) {
tLRLinearLayout = layout;
Context context = layout.getContext();
initAttrs(context, attrs);
mStatusController = new TLRStatusController(this, context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TLRLinearLayout);
if (array == null) {
TLRLog.e("initAttrs array is null");
return;
}
try {
final int N = array.getIndexCount();
for (int i = 0; i < N; i++) {
int index = array.getIndex(i);
if (index == R.styleable.TLRLinearLayout_refreshThreshold) {
mRefreshThreshold = array.getFloat(index, mRefreshThreshold);
} else if (index == R.styleable.TLRLinearLayout_loadThreshold) {
mLoadThreshold = array.getFloat(index, mLoadThreshold);
} else if (index == R.styleable.TLRLinearLayout_resistance) {
mResistance = array.getFloat(index, mResistance);
} else if (index == R.styleable.TLRLinearLayout_closeAnimDuration) {
mCloseAnimDuration = array.getInt(index, mCloseAnimDuration);
} else if (index == R.styleable.TLRLinearLayout_openAnimDuration) {
mOpenAnimDuration = array.getInt(index, mOpenAnimDuration);
} else if (index == R.styleable.TLRLinearLayout_keepHeadRefreshing) {
isKeepHeadRefreshing = array.getBoolean(index, isKeepHeadRefreshing);
} else if (index == R.styleable.TLRLinearLayout_keepFootLoading) {
isKeepFootLoading = array.getBoolean(index, isKeepFootLoading);
} else if (index == R.styleable.TLRLinearLayout_refreshMaxMoveDistance) {
mRefreshMaxMoveDistance = array.getDimensionPixelOffset(index,
mRefreshMaxMoveDistance);
} else if (index == R.styleable.TLRLinearLayout_loadMaxMoveDistance) {
mLoadMaxMoveDistance = array.getDimensionPixelOffset(index,
mLoadMaxMoveDistance);
}
}
} finally {
array.recycle();
}
TLRLog.v("isKeepHeadRefreshing = " + isKeepHeadRefreshing);
TLRLog.v("isKeepFootLoading = " + isKeepFootLoading);
TLRLog.v("mRefreshMaxMoveDistance = " + mRefreshMaxMoveDistance);
TLRLog.v("mLoadMaxMoveDistance = " + mLoadMaxMoveDistance);
}
public void setTLRUiHandler(TLRUIHandler uiHandler) {
mTLRUiHandler = uiHandler;
mStatusController.setTLRUiHandler(mTLRUiHandler);
}
public void setHeadViewHeight(int height) {
mHeadHeight = height;
refreshThresholdHeight = (int) (mHeadHeight * mRefreshThreshold);
}
public void setFootViewHeight(int height) {
mFootHeight = height;
loadThresholdHeight = (int) (mFootHeight * mLoadThreshold);
}
/**
* call by layout touch down
*/
public void eventDown(float x, float y) {
mLastX = mDownX = x;
mLastY = mDownY = y;
}
/**
* call by layout touch move
*/
public void eventMove(float x, float y) {
float xDiff = x - mLastX;
float yDiff = y - mLastY;
setDirection(xDiff, yDiff);
setOffset(xDiff, yDiff);
mLastX = x;
mLastY = y;
}
/**
* call by layout touch up/cancel
*/
public void eventUp(float x, float y) {
mDirection = Direction.NONE;
mStatusController.calculatorUpRefreshStatus();
mStatusController.calculatorUpLoadStatus();
if (isKeepFootLoading && mTotalOffsetY <= -loadThresholdHeight) {
startKeepAnimator();
} else if (isKeepHeadRefreshing && mTotalOffsetY >= refreshThresholdHeight) {
startKeepAnimator();
} else {
startResetAnimator();
}
}
/**
* eventMove distance must more than {@link ViewConfiguration#getScaledTouchSlop()}
*/
public boolean canCalculatorV() {
if (mDirection == Direction.DOWN || mDirection == Direction.UP) {
return Math.abs(mLastY - mDownY) > mTouchSlop;
}
return false;
}
public void touchMoveLayoutView() {
touchMoveLayoutView((int) mOffsetY);
}
public void touchMoveLayoutView(int offsetY) {
moveOffsetY(offsetY);
}
/**
* call view {@link android.view.View#offsetTopAndBottom(int)} method must cast offset to int
*/
private void moveOffsetY(int y) {
if (y == 0) {
return;
}
//start ui move
if (mTotalOffsetY == 0 && isBackStatus) {
isBackStatus = false;
}
y = calculateMaxMoveDistance(y, mTotalOffsetY);
if (y == 0) {
return;
}
//TLRLog.d("mTotalOffsetY:" + mTotalOffsetY + " y:" + y);
//move view
mTotalOffsetY += y;
tLRLinearLayout.move(y);
//calculate move status
mStatusController.calculateMoveRefreshStatus(y > 0);
mStatusController.calculateMoveLoadStatus(y < 0);
//notify offset
if (mTotalOffsetY > 0 && mStatusController.getLoadStatus() == LoadStatus.IDLE) {
notifyPixOffset(mTotalOffsetY, refreshThresholdHeight, y);
}
if (mTotalOffsetY < 0 && mStatusController.getRefreshStatus() == RefreshStatus.IDLE) {
notifyPixOffset(mTotalOffsetY, loadThresholdHeight, y);
}
//end ui move
if (mTotalOffsetY == 0 && !isBackStatus) {
isBackStatus = true;
notifyPixOffset(0, 0, y);
}
}
private int calculateMaxMoveDistance(int y, int totalOffsetY) {
int tempTotalOffsetY = totalOffsetY + y;
// calculate refresh over max move distance
if (tempTotalOffsetY > 0 && mRefreshMaxMoveDistance > 0
&& tempTotalOffsetY > mRefreshMaxMoveDistance) {
y = mRefreshMaxMoveDistance - mTotalOffsetY;
}
// calculate load over max move distance
if (tempTotalOffsetY < 0 && mLoadMaxMoveDistance > 0
&& -tempTotalOffsetY > mLoadMaxMoveDistance) {
y = -mLoadMaxMoveDistance - mTotalOffsetY;
}
return y;
}
private void notifyPixOffset(int totalOffsetY, int height, int y) {
int totalThresholdY = totalOffsetY;
if (Math.abs(totalThresholdY) >= height) {
totalThresholdY = height * Integer.signum(totalThresholdY);
}
float offset = 0.0f;
if (totalThresholdY != 0) {
offset = (float) (Math.round(((float) totalThresholdY / height) * 100)) / 100;
}
boolean isRefresh = totalOffsetY != 0 ? totalOffsetY > 0 : y < 0;
mTLRUiHandler.onOffsetChanged(tLRLinearLayout.getTouchView(), isRefresh, totalOffsetY,
totalThresholdY, y, offset);
}
private void setDirection(float xDiff, float yDiff) {
if (Math.abs(xDiff) > Math.abs(yDiff)) {
if (xDiff > 0) {
mDirection = Direction.RIGHT;
} else {
mDirection = Direction.LEFT;
}
} else {
if (yDiff > 0) {
mDirection = Direction.DOWN;
} else {
mDirection = Direction.UP;
}
}
}
private void setOffset(float offsetX, float offsetY) {
mOffsetX = offsetX / mResistance;
mOffsetY = offsetY / mResistance;
}
public void startAutoRefresh() {
TLRLog.d("autoRefresh mHeadHeight:" + mHeadHeight);
mStatusController.setAutoRefreshing(true);
if (mHeadHeight == 0) {
tLRLinearLayout.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
TLRLog.d("autoRefresh onGlobalLayout mHeadHeight:" + mHeadHeight);
startAutoRefreshAnimator();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
tLRLinearLayout.getViewTreeObserver().removeOnGlobalLayoutListener(
this);
} else {
tLRLinearLayout.getViewTreeObserver().removeGlobalOnLayoutListener(
this);
}
}
});
} else {
startAutoRefreshAnimator();
}
}
private void startAutoRefreshAnimator() {
if (mHeadHeight != 0) {
endAllRunningAnimator();
int startY = -mHeadHeight;
mAutoAnimator = ValueAnimator.ofInt(startY, 0);
mAutoAnimator.setDuration(mOpenAnimDuration);
mAutoAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mAutoAnimator.addUpdateListener(new AnimUpdateListener(startY));
mAutoAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAutoAnimator.removeListener(this);
TLRLog.v("startAutoRefreshAnimator isKeepHeadRefreshing:"
+ isKeepHeadRefreshing);
if (isKeepHeadRefreshing) {
startKeepAnimator();
} else {
startResetAnimator();
}
}
});
mAutoAnimator.start();
}
}
public boolean hasAnyAnimatorRunning() {
if (mAutoAnimator != null) {
return mAutoAnimator.isRunning() || mAutoAnimator.isStarted();
}
if (mResetAnimator != null) {
return mResetAnimator.isRunning() || mResetAnimator.isStarted();
}
if (mKeepAnimator != null) {
return mKeepAnimator.isRunning() || mKeepAnimator.isStarted();
}
return false;
}
private void startKeepAnimator() {
endAllRunningAnimator();
int startY = mTotalOffsetY;
int endY;
if (mTotalOffsetY > 0) {
endY = refreshThresholdHeight;
} else {
endY = -loadThresholdHeight;
}
if (DEBUG) {
TLRLog.d("startKeepAnimator startY:" + startY + " endY:" + endY);
}
if (startY != 0 && startY != endY) {
mKeepAnimator = ValueAnimator.ofInt(startY, endY);
mKeepAnimator.setDuration(200);
mKeepAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mKeepAnimator.addUpdateListener(new AnimUpdateListener(startY));
mKeepAnimator.start();
}
}
private void startResetAnimator() {
if (mTotalOffsetY != 0) {
endAllRunningAnimator();
int startY = mTotalOffsetY;
mResetAnimator = ValueAnimator.ofInt(startY, 0);
long duration = (long) (mCloseAnimDuration * ((float) Math.abs(mTotalOffsetY)
/ mHeadHeight));
mResetAnimator.setDuration(duration);
mResetAnimator.setInterpolator(new DecelerateInterpolator());
mResetAnimator.addUpdateListener(new AnimUpdateListener(startY));
mResetAnimator.start();
}
}
private void endAllRunningAnimator() {
if (mAutoAnimator != null && mAutoAnimator.isRunning()) {
mAutoAnimator.end();
}
if (mResetAnimator != null && mResetAnimator.isRunning()) {
mResetAnimator.end();
}
if (mKeepAnimator != null && mKeepAnimator.isRunning()) {
mKeepAnimator.end();
}
}
public int getTotalOffsetY() {
return mTotalOffsetY;
}
public Direction getDirection() {
return mDirection;
}
public void finishRefresh(boolean isSuccess, int errorCode) {
if (mStatusController.isAutoRefreshing()) {
mStatusController.setAutoRefreshing(false);
}
if (isKeepHeadRefreshing) {
hookKeepView();
}
mStatusController.finishRefresh();
mTLRUiHandler.onFinish(tLRLinearLayout.getTouchView(), true, isSuccess, errorCode);
}
public void finishLoad(boolean isSuccess, int errorCode) {
if (isKeepFootLoading) {
hookKeepView();
}
mStatusController.finishLoad();
mTLRUiHandler.onFinish(tLRLinearLayout.getTouchView(), false, isSuccess, errorCode);
}
private void hookKeepView() {
if (mHooks.size() != 0) {
for (TLRUIHandlerHook hook : mHooks) {
hook.handlerHook();
}
} else {
resetKeepView();
}
}
private void resetKeepView() {
startResetAnimator();
}
public void hook(TLRUIHandlerHook hook) {
if (hook != null) {
mHooks.add(hook);
}
}
public void releaseHook(TLRUIHandlerHook hook) {
if (hook != null) {
mHooks.remove(hook);
}
if (mHooks.size() == 0) {
resetKeepView();
}
}
public float getRefreshThreshold() {
return mRefreshThreshold;
}
public void setRefreshThreshold(float refreshThreshold) {
mRefreshThreshold = refreshThreshold;
setHeadViewHeight(mHeadHeight);
}
public float getLoadThreshold() {
return mLoadThreshold;
}
public void setLoadThreshold(float loadThreshold) {
mLoadThreshold = loadThreshold;
setFootViewHeight(mFootHeight);
}
public int getRefreshMaxMoveDistance() {
return mRefreshMaxMoveDistance;
}
public void setRefreshMaxMoveDistance(int refreshMaxMoveDistance) {
mRefreshMaxMoveDistance = refreshMaxMoveDistance;
}
public int getLoadMaxMoveDistance() {
return mLoadMaxMoveDistance;
}
public void setLoadMaxMoveDistance(int loadMaxMoveDistance) {
mLoadMaxMoveDistance = loadMaxMoveDistance;
}
public float getResistance() {
return mResistance;
}
public void setResistance(float resistance) {
mResistance = resistance;
}
public void setCloseAnimDuration(int closeAnimDuration) {
mCloseAnimDuration = closeAnimDuration;
}
public void setOpenAnimDuration(int openAnimDuration) {
mOpenAnimDuration = openAnimDuration;
}
public boolean isKeepHeadRefreshing() {
return isKeepHeadRefreshing;
}
public void setKeepHeadRefreshing(boolean keepHeadRefreshing) {
isKeepHeadRefreshing = keepHeadRefreshing;
}
public boolean isKeepFootLoading() {
return isKeepFootLoading;
}
public void setKeepFootLoading(boolean keepFootLoading) {
isKeepFootLoading = keepFootLoading;
}
public boolean isReleaseRefresh() {
return mStatusController.isReleaseRefresh();
}
public void setReleaseRefresh(boolean releaseRefresh) {
mStatusController.setReleaseRefresh(releaseRefresh);
}
public boolean isReleaseLoad() {
return mStatusController.isReleaseLoad();
}
public void setReleaseLoad(boolean releaseLoad) {
mStatusController.setReleaseLoad(releaseLoad);
}
public boolean isAutoRefreshing() {
return mStatusController.isAutoRefreshing();
}
public boolean isRefreshing() {
return mStatusController.isRefreshing();
}
public boolean isLoading() {
return mStatusController.isLoading();
}
public boolean isBackStatus() {
return isBackStatus;
}
public boolean isRefresh() {
return getTotalOffsetY() > 0;
}
public boolean isLoad() {
return getTotalOffsetY() < 0;
}
private class AnimUpdateListener implements ValueAnimator.AnimatorUpdateListener {
private int lastY;
AnimUpdateListener(int startY) {
this.lastY = startY;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
moveOffsetY(value - lastY);
lastY = value;
}
}
public enum Direction {
LEFT,
RIGHT,
UP,
DOWN,
NONE
}
}