问题描述
pulltorefresh这种下拉刷新,上拉加载的控件用的很多,可是有思考过是怎样实现的吗?现在我们来逐步分析解决方案
首先我们实现下拉上拉的功能,并且将状态回调回去。这篇博客我们先分析https://github.com/HomHomLin/SlidingLayout,下篇博客我们分析android-Ultra-Pull-to-Refresh(增加一个header 显示下拉的不同状态)
首先上一张图看下效果
实现原理:SlidingLayout(充当背景)包含子控件listview、recycleview或者webview,当拖拽子控件时,控件处于最上边或者最下边,onInterceptTouchEvent返回true拦截事件,并且交由onTouchEvent消费事件,让子veiw根据手势位置做相关的属性动画移动,当松手时,恢复到原始位置。
那么我们需要实现onInterceptTouchEvent、onTouchEvent方法,并且根据手势,进行相关的动画操作,并且将状态回调回去。
首先SlidingLayout可以包含子view,必定是viewgroup的子类,如果继承viewgroup,则需要实现onlayout等方法,这里没有必要,我们直接继承FrameLayout
自定义一部分属性
<declare-styleable name="SlidingLayout">
<attr name="background_view" format="reference" />
<attr name="sliding_mode" format="enum">
<enum name="both" value="0" />
<enum name="top" value="1" />
<enum name="bottom" value="2" />
</attr>
<attr name="sliding_pointer_mode" format="enum">
<enum name="one" value="0" />
<enum name="more" value="1" />
</attr>
<attr name="top_max" format="dimension" />
</declare-styleable>
background_view这个自定义SlidingLayout背景,sliding_mode上拉、下拉,sliding_pointer_mode多点触控
构造方法和初始化方法,初始化自定义属性,初始化背景view,初始化mTouchSlop(系统允许最小的滑动判断值)
public SlidingLayout(Context context) {
this(context, null);
}
public SlidingLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingLayout);
mBackgroundViewLayoutId = a.getResourceId(R.styleable.SlidingLayout_background_view, mBackgroundViewLayoutId);
mSlidingMode = a.getInteger(R.styleable.SlidingLayout_sliding_mode, SLIDING_MODE_BOTH);
mSlidingPointerMode = a.getInteger(R.styleable.SlidingLayout_sliding_pointer_mode, SLIDING_POINTER_MODE_MORE);
mSlidingTopMaxDistance = a.getDimensionPixelSize(R.styleable.SlidingLayout_top_max, SLIDING_DISTANCE_UNDEFINED);
a.recycle();
if (mBackgroundViewLayoutId != 0) {
View view = View.inflate(getContext(), mBackgroundViewLayoutId, null);
setBackgroundView(view);
}
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
其他一些set,get方法就不多讲了,重点就是setlistener方法
public void setSlidingListener(SlidingListener slidingListener) {
this.mSlidingListener = slidingListener;
}
回收内存处理,清楚动画效果
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mTargetView != null) {
mTargetView.clearAnimation();
}
mSlidingMode = 0;
mTargetView = null;
mBackgroundView = null;
mSlidingListener = null;
}
核心的onInterceptTouchEvent方法,是否拦截事件,如果返回true,交由onTouchEvent处理事件
首先初始化子view,获取存储子view数组当中最后一个view,但是我们大部分使用场景只是一个子view
ensureTarget();
拿到事件的action,然后switch分支判断ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL分别处理,当按下,我们需要拿到触点列表,并且以第0个触点为准,并且初始化当前的y坐标
final int action = MotionEventCompat.getActionMasked(ev);
//判断拦截
switch (action) {
case MotionEvent.ACTION_DOWN:
// Log.i("onInterceptTouchEvent", "down");
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
return false;
}
mInitialDownY = initialDownY;
break;
注意:如果PointerId=-1,说明触点列表为null的,那么直接返回。
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
// Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
// 拿到当前触点的y值
final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
if (y > mInitialDownY) {
//判断是否是上拉操作
final float yDiff = y - mInitialDownY; // 变化值
// 变化值大于系统最小移动系数,子view处在最底端,当前为处于未拖拽状态
if (yDiff > mTouchSlop && !mIsBeingDragged && !canChildScrollUp()) {
mInitialMotionY = mInitialDownY + mTouchSlop;
// 记录最后y位置
mLastMotionY = mInitialMotionY;
mIsBeingDragged = true;
}
} else if (y < mInitialDownY) {
//判断是否是下拉操作
final float yDiff = mInitialDownY - y;
if (yDiff > mTouchSlop && !mIsBeingDragged && !canChildScrollDown()) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mLastMotionY = mInitialMotionY;
mIsBeingDragged = true;
}
}
break;
当子view第一次处在顶端或者底端时,我们将mIsBeingDragged置为true,下次就无法再满足!mIsBeingDragged这个条件
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Log.i("onInterceptTouchEvent", "up");
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
当up或者cancel,将mIsBeingDragged置为false。
最后,返回return mIsBeingDragged,我们知道只有当move状态下,第一次满足子view处在顶端或者底端才将其置为true。当为true时,事件被拦截,交由onTouchEvent处理,其他时候都是返回false,交由子view处理事件
return mIsBeingDragged;
那么onTouchEvent方法消费事件,也是对MotionEvent做相关的处理。由onInterceptTouchEvent可知,只有当move时候,并且子view处在顶端或者底端,才消费事件,所以我们只需要在onTouchEvent消费事件。
if (mSlidingPointerMode == SLIDING_POINTER_MODE_MORE) {
//homhom:it's different betweenn more than one pointer
// 触点列表最后一个id
int activePointerId = MotionEventCompat.getPointerId(event, event.getPointerCount() - 1);
// 变换了触点,将更新现在的触点,更新触点y坐标
if (mActivePointerId != activePointerId) {
//change pointer
// Log.i("onTouchEvent","change point");
mActivePointerId = activePointerId;
mInitialDownY = getMotionEventY(event, mActivePointerId);
mInitialMotionY = mInitialDownY + mTouchSlop;
mLastMotionY = mInitialMotionY;
if (mSlidingListener != null) {
// 回调触点id
mSlidingListener.onSlidingChangePointer(mTargetView, activePointerId);
}
}
//pointer delta
// delta = getInstrument().getTranslationY(mTargetView)
// + ((getMotionEventY(event, mActivePointerId) - mLastMotionY))
// / mSlidingOffset;
// 触点偏移量
delta = getMotionEventY(event, mActivePointerId) - mLastMotionY;
//滑动阻力计算
// float tempOffset = getInstrument().getTranslationY(mTargetView)
// + delta;
// 计算滑动阻力,当控件滑动越多tempOffset越大,tempOffset值越大阻力越大
// getInstrument().getTranslationY(mTargetView)当前动画偏移量
float tempOffset = 1 - (Math.abs(getInstrument().getTranslationY(mTargetView)
+ delta) / mTargetView.getMeasuredHeight());
// 子view最终根据触点偏移量,触点移动越大,值越小
delta = getInstrument().getTranslationY(mTargetView)
+ delta * mSlidingOffset * tempOffset;
// 更新最新y轴位置
mLastMotionY = getMotionEventY(event, mActivePointerId);
//used for judge which side move to
// 触点移动变化值,可正可负
movemment = getMotionEventY(event, mActivePointerId) - mInitialMotionY;
} else {
// 单触点情况下,不存在触点变换
float tempOffset = 1 - Math.abs(getInstrument().getTranslationY(mTargetView) / mTargetView.getMeasuredHeight());
delta = (event.getY() - mInitialMotionY) * mSlidingOffset * tempOffset;
//used for judge which side move to
movemment = event.getY() - mInitialMotionY;
}
多点触控和单点触控,计算根据触点偏移量
float distance = getSlidingDistance();
switch (mSlidingMode) {
case SLIDING_MODE_BOTH:
getInstrument().slidingByDelta(mTargetView, delta);
break;
case SLIDING_MODE_TOP:
if (movemment >= 0 || distance > 0) {
//向下滑动
if (delta < 0) {
//如果还往上滑,就让它归零
delta = 0;
}
if (mSlidingTopMaxDistance == SLIDING_DISTANCE_UNDEFINED || delta < mSlidingTopMaxDistance) {
//滑动范围内 for todo
} else {
//超过滑动范围
delta = mSlidingTopMaxDistance;
}
getInstrument().slidingByDelta(mTargetView, delta);
}
break;
case SLIDING_MODE_BOTTOM:
if (movemment <= 0 || distance < 0) {
//向上滑动
if (delta > 0) {
//如果还往下滑,就让它归零
delta = 0;
}
getInstrument().slidingByDelta(mTargetView, delta);
}
break;
}
if (mSlidingListener != null) {
mSlidingListener.onSlidingStateChange(this, STATE_SLIDING);
mSlidingListener.onSlidingOffset(this, delta);
}
break;
根据设置的top和bottom判断给子view设置动画移动值
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Log.i("onTouchEvent", "up");
if (mSlidingListener != null) {
mSlidingListener.onSlidingStateChange(this, STATE_IDLE);
}
getInstrument().reset(mTargetView, RESET_DURATION);
break;
在up和cancel中恢复原始位置
调用就很简单了
<com.song.testslidinglayout.SlidingLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/slidingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:sliding_pointer_mode="more"
app:sliding_mode="both"
app:background_view="@layout/view_bg">
<ListView
android:id="@+id/listview"
android:background="#ffffff"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</com.song.testslidinglayout.SlidingLayout>
并且实现状态监听方法。
了解了这种实现核心的onInterceptTouchEvent方法,onTouchEvent
方法,操作子view,下一篇博客我们实现更多功能,真正刷新的layout。
/**
* --------------
* 欢迎转载 | 转载请注明
* --------------
* 如果对你有帮助,请点击|顶|
* --------------
* 请保持谦逊 | 你会走的更远
* --------------
* @author css
* @github https://github.com/songsongbrother
* @blog http://blog.csdn.net/xiangxi101
*/