package com.lzy.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout;
import android.widget.Scroller;
public class HeaderViewPager extends LinearLayout {
private static final int DIRECTION_UP = 1;
private static final int DIRECTION_DOWN = 2;
private int topOffset = 0; //滚动的最大偏移量
private Scroller mScroller;
private int mTouchSlop; //表示滑动的时候,手的移动要大于这个距离才开始移动控件。
private int mMinimumVelocity; //允许执行一个fling手势动作的最小速度值
private int mMaximumVelocity; //允许执行一个fling手势动作的最大速度值
private int sysVersion; //当前sdk版本,用于判断api版本
private View mHeadView; //需要被滑出的头部
private int mHeadHeight; //滑出头部的高度
private int maxY = 0; //最大滑出的距离,等于 mHeadHeight
private int minY = 0; //最小的距离, 头部在最顶部
private int mCurY; //当前已经滚动的距离
private VelocityTracker mVelocityTracker;
private int mDirection;
private int mLastScrollerY;
private boolean mDisallowIntercept; //是否允许拦截事件
private boolean isClickHead; //当前点击区域是否在头部
private OnScrollListener onScrollListener; //滚动的监听
private HeaderScrollHelper mScrollable;
public interface OnScrollListener {
void onScroll(int currentY, int maxY);
}
public void setOnScrollListener(OnScrollListener onScrollListener) {
this.onScrollListener = onScrollListener;
}
public HeaderViewPager(Context context) {
this(context, null);
}
public HeaderViewPager(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HeaderViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HeaderViewPager);
topOffset = a.getDimensionPixelSize(a.getIndex(R.styleable.HeaderViewPager_hvp_topOffset), topOffset);
a.recycle();
mScroller = new Scroller(context);
mScrollable = new HeaderScrollHelper();
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop(); //表示滑动的时候,手的移动要大于这个距离才开始移动控件。
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); //允许执行一个fling手势动作的最小速度值
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); //允许执行一个fling手势动作的最大速度值
sysVersion = Build.VERSION.SDK_INT;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (mHeadView != null && !mHeadView.isClickable()) {
mHeadView.setClickable(true);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mHeadView = getChildAt(0);
measureChildWithMargins(mHeadView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0);
mHeadHeight = mHeadView.getMeasuredHeight();
maxY = mHeadHeight - topOffset;
//让测量高度加上头部的高度
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + maxY, MeasureSpec.EXACTLY));
}
/** @param disallowIntercept 作用同 requestDisallowInterceptTouchEvent */
public void requestHeaderViewPagerDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
mDisallowIntercept = disallowIntercept;
}
private float mDownX; //第一次按下的x坐标
private float mDownY; //第一次按下的y坐标
private float mLastY; //最后一次移动的Y坐标
private boolean verticalScrollFlag = false; //是否允许垂直滚动
/**
* 说明:一旦dispatTouchEvent返回true,即表示当前View就是事件传递需要的 targetView,事件不会再传递给
* 其他View,如果需要将事件继续传递给子View,可以手动传递
* 由于dispatchTouchEvent处理事件的优先级高于子View,也高于onTouchEvent,所以在这里进行处理
* 好处一:当有子View,并且子View可以获取焦点的时候,子View的onTouchEvent会优先处理,如果当前逻辑
* 在onTouchEnent中,则事件无法到达,逻辑失效
* 好处二:当子View是拥有滑动事件时,例如ListView,ScrollView等,不需要对子View的事件进行拦截,可以
* 全部让该父控件处理,在需要的地方手动将事件传递给子View,保证滑动的流畅性,结尾两行代码就是证明:
* super.dispatchTouchEvent(ev);
* return true;
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float currentX = ev.getX(); //当前手指相对于当前view的X坐标
float currentY = ev.getY(); //当前手指相对于当前view的Y坐标
float shiftX = Math.abs(currentX - mDownX); //当前触摸位置与第一次按下位置的X偏移量
float shiftY = Math.abs(currentY - mDownY); //当前触摸位置与第一次按下位置的Y偏移量
float deltaY; //滑动的偏移量,即连续两次进入Move的偏移量
obtainVelocityTracker(ev); //初始化速度追踪器
switch (ev.getAction()) {
//Down事件主要初始化变量
case MotionEvent.ACTION_DOWN:
mDisallowIntercept = false;
verticalScrollFlag = false;
mDownX = currentX;
mDownY = currentY;
mLastY = currentY;
checkIsClickHead((int) currentY, mHeadHeight, getScrollY());
mScroller.abortAnimation();
break;
case MotionEvent.ACTION_MOVE:
if (mDisallowIntercept) break;
deltaY = mLastY - currentY; //连续两次进入move的偏移量
mLastY = currentY;
if (shiftX > mTouchSlop && shiftX > shiftY) {
//水平滑动
verticalScrollFlag = false;
} else if (shiftY > mTouchSlop && shiftY > shiftX) {
//垂直滑动
verticalScrollFlag = true;
}
/**
* 这里要注意,对于垂直滑动来说,给出以下三个条件
* 头部没有固定,允许滑动的View处于第一条可见,当前按下的点在头部区域
* 三个条件满足一个即表示需要滚动当前布局,否者不处理,将事件交给子View去处理
*/
if (verticalScrollFlag && (!isStickied() || mScrollable.isTop() || isClickHead)) {
//如果是向下滑,则deltaY小于0,对于scrollBy来说
//正值为向上和向左滑,负值为向下和向右滑,这里要注意
scrollBy(0, (int) (deltaY + 0.5));
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (verticalScrollFlag) {
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); //1000表示单位,每1000毫秒允许滑过的最大距离是mMaximumVelocity
float yVelocity = mVelocityTracker.getYVelocity(); //获取当前的滑动速度
mDirection = yVelocity > 0 ? DIRECTION_DOWN : DIRECTION_UP; //下滑速度大于0,上滑速度小于0
//根据当前的速度和初始化参数,将滑动的惯性初始化到当前View,至于是否滑动当前View,取决于computeScroll中计算的值
//这里不判断最小速度,确保computeScroll一定至少执行一次
mScroller.fling(0, getScrollY(), 0, -(int) yVelocity, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
mLastScrollerY = getScrollY();
invalidate(); //更新界面,该行代码会导致computeScroll中的代码执行
//阻止快读滑动的时候点击事件的发生,滑动的时候,将Up事件改为Cancel就不会发生点击了
if ((shiftX > mTouchSlop || shiftY > mTouchSlop)) {
if (isClickHead || !isStickied()) {
int action = ev.getAction();
ev.setAction(MotionEvent.ACTION_CANCEL);
boolean dd = super.dispatchTouchEvent(ev);
ev.setAction(action);
return dd;
}
}
}
recycleVelocityTracker();
break;
case MotionEvent.ACTION_CANCEL:
recycleVelocityTracker();
break;
default:
break;
}
//手动将事件传递给子View,让子View自己去处理事件
super.dispatchTouchEvent(ev);
//消费事件,返回True表示当前View需要消费事件,就是事件的TargetView
return true;
}
private void checkIsClickHead(int downY, int headHeight, int scrollY) {
isClickHead = ((downY + scrollY) <= headHeight);
}
private void obtainVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int currY = mScroller.getCurrY();
if (mDirection == DIRECTION_UP) {
// 手势向上划
if (isStickied()) {
//这里主要是将快速滚动时的速度对接起来,让布局看起来滚动连贯
int distance = mScroller.getFinalY() - currY; //除去布局滚动消耗的时间后,剩余的时间
int duration = calcDuration(mScroller.getDuration(), mScroller.timePassed()); //除去布局滚动的距离后,剩余的距离
mScrollable.smoothScrollBy(getScrollerVelocity(distance, duration), distance, duration);
//外层布局已经滚动到指定位置,不需要继续滚动了
mScroller.abortAnimation();
return;
} else {
scrollTo(0, currY); //将外层布局滚动到指定位置
invalidate(); //移动完后刷新界面
}
} else {
// 手势向下划,内部View已经滚动到顶了,需要滚动外层的View
if (mScrollable.isTop() || isClickHead) {
int deltaY = (currY - mLastScrollerY);
int toY = getScrollY() + deltaY;
scrollTo(0, toY);
if (mCurY <= minY) {
mScroller.abortAnimation();
return;
}
}
//向下滑动时,初始状态可能不在顶部,所以要一直重绘,让computeScroll一直调用
//确保代码能进入上面的if判断
invalidate();
}
mLastScrollerY = currY;
}
}
@SuppressLint("NewApi")
private int getScrollerVelocity(int distance, int duration) {
if (mScroller == null) {
return 0;
} else if (sysVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return (int) mScroller.getCurrVelocity();
} else {
return distance / duration;
}
}
/** 对滑动范围做限制 */
@Override
public void scrollBy(int x, int y) {
int scrollY = getScrollY();
int toY = scrollY + y;
if (toY >= maxY) {
toY = maxY;
} else if (toY <= minY) {
toY = minY;
}
y = toY - scrollY;
super.scrollBy(x, y);
}
/** 对滑动范围做限制 */
@Override
public void scrollTo(int x, int y) {
if (y >= maxY) {
y = maxY;
} else if (y <= minY) {
y = minY;
}
mCurY = y;
if (onScrollListener != null) {
onScrollListener.onScroll(y, maxY);
}
super.scrollTo(x, y);
}
/** 头部是否已经固定 */
public boolean isStickied() {
return mCurY == maxY;
}
private int calcDuration(int duration, int timepass) {
return duration - timepass;
}
public int getMaxY() {
return maxY;
}
public boolean isHeadTop() {
return mCurY == minY;
}
/** 是否允许下拉,与PTR结合使用 */
public boolean canPtr() {
return verticalScrollFlag && mCurY == minY && mScrollable.isTop();
}
public void setTopOffset(int topOffset) {
this.topOffset = topOffset;
}
public void setCurrentScrollableContainer(HeaderScrollHelper.ScrollableContainer scrollableContainer) {
mScrollable.setCurrentScrollableContainer(scrollableContainer);
}
}