接上一篇https://www.jianshu.com/p/a3fed8b73625
给ListView加上顶端和底端的弹性拉动效果。
-
当加上item点击事件后,发现在顶端时候,如果向下拉,这时候会有拉伸效果,但松手后会发现一定会触发item点击事件,不符合用户习惯,体验也不好,如图:
old.gif
可以看到在松手后,弹出了item点击提示。
-
用什么办法解决呢?
刚开始的想法:当手指触摸到item的时候,item就有背景色表示被选中,那这个时候是不是相当于item子View在touch相关事件里面做了处理并消费了事件呢?。但反过来想根本不是这样的:1.如果item消费了触摸一系列事件,那么listview的onTouchevent根本不可能执行,更不可能在里面操作我们的拉伸效果。2.经过验证,这里item里面只有一个TextView,而且TextView默认是不消费触摸事件的,除非给它单独加上可点击属性。
那说明ListView是自己处理了item点击事件,和item的点击没有什么关系。查看源码, 发现ListView唯一处理了触摸相关事件的只有在基类AbsListView里面复写了onTouchEvent方法,而且发现里面的onTouchUp方法里面最终会调用基类AdapterView里面的performItemClick方法,触发mOnItemClickListener.onItemClick。
刚开始直接在ListView的onTouchEvent的ActionDown事件的时候,直接返回true,不然父类做选中item的操作,但这时候发现整个列表都无法点击,而且ActionDown是事件序列的开始,父类做了大量操作,肯定有较大影响。
既然是ActionUp时候在父类触发了item点击事件,那么直接在ActionUp返回false或者true,不调用父类的onTouchEvent方法行不行呢?尝试后发现也是不行的,这时候一样会导致列表不能正常滑动而且选中的列表的背景颜色也不消失等一些奇怪的现象。
再尝试在ActionUp的时候,将事件Action手动设置为ACTION_CANCEL,并作为参数调用父类onTouchEvent,终于达到效果。怀疑这里,父类在判断如果是取消事件的话,当然不会再做点击操作,同时自然结束这次触摸。但应该注意的是:这里应该计算一下整个一次触摸事件序列的滑动总距离,如果小于最小滑动距离(ViewConfiguration.get(getContext()).getScaledTouchSlop())就认为是点击事件(正常调用父类onTouchEvent),否则就认为是滑动事件(调用父类onTouchEvent,但事件参数设置为ACTION_CANCEL)。如果不这样处理会出现虽然滑动到顶端或者底端,但这时候用户是真的想点击item却无法收到点击效果。
改正后效果如图:
fixed.gif
改正后完整源码:
package com.h.anthony.widgets;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.Scroller;
/**
* Created by hf on 2020-06-17.
* <p>
* {@link Scroller}或者{@link android.view.ViewGroup.MarginLayoutParams}实现ListView的上下弹性拉伸
* <p>
* <p>
* 其实用@{@link View#setTranslationY(float)} }也可以实现拉伸,但每次拉伸的时候边界总是有闪烁的情况,
* 未找到原因,这里就不实现这种方式了。
*/
public class TelescopingListView extends ListView implements AbsListView.OnScrollListener {
private static final String TAG = "WechatListView";
/**
* 标记是否滑到了最顶端
*/
private boolean mTopArrived = true;
/**
* 标记是否滑到了最底端
*/
private boolean mBottomArrived = false;
/**
* 处理相关回弹效果的工具类,包含两种实现方式
*/
private TelescopTool mTelescopTool;
/**
* 滑动的最小距离,小于这个距离不认为是在滑动,跟具体的设备有关
*/
private static int MINSCROLLDISTANCE;
public TelescopingListView(Context context, AttributeSet attrs) {
super(context, attrs);
MINSCROLLDISTANCE = ViewConfiguration.get(getContext()).getScaledTouchSlop();
setOverScrollMode(View.OVER_SCROLL_NEVER);
setOnScrollListener(this);
// mTelescopTool = new ScrollerTelescopTool(this);
mTelescopTool = new MarginTelescopTool(this);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mTelescopTool.listViewLayoutChange();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean ret = mTelescopTool.dealTouchEvent(mTopArrived, mBottomArrived, ev) ? true : super.onTouchEvent(ev);
Log.e(TAG, "onTouchEvent: " + ev.getAction() + ";" + ret);
return ret;
}
@Override
public void computeScroll() {
mTelescopTool.computeScroll();
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
/**
* @param view
* @param firstVisibleItem
* @param visibleItemCount
* @param totalItemCount
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View firstChildView = getChildAt(0);
if (firstChildView != null) {
int top = firstChildView.getTop();
if (top == 0 && firstVisibleItem == 0) {//滑到最顶端
mTopArrived = true;
Log.d(TAG, "滑到了最顶端");
} else {
mTopArrived = false;
}
} else {
mTopArrived = false;
}
View lastChildView = getChildAt(getChildCount() - 1);
if (lastChildView != null) {
int bottom = lastChildView.getBottom();
if (bottom == getHeight() && (firstVisibleItem + visibleItemCount == totalItemCount)) {//滑到最低端
mBottomArrived = true;
Log.d(TAG, "滑到了最低端");
} else {
mBottomArrived = false;
}
} else {
mBottomArrived = false;
}
}
/**
* 抽象类
*/
private static abstract class TelescopTool {
/**
* 阻尼参数(0,1]越小拉动越费力,其实就是把实际拉伸的距离*这个参数。
*/
private final static float DAMPING = 0.5f;
/**
* 记录当次触摸事件序列总共滑动的距离,如果小于这个距离,在手指抬起的时候会做点击处理,具体见ActionUp里面的处理
*/
protected float mmDeltaYSum;//postive;
protected TelescopingListView telescopingListView;
/**
* 记录上一次手指距离屏幕边缘的Y坐标
*/
protected float mmLastRawY;
public TelescopTool(TelescopingListView telescopingListView) {
this.telescopingListView = telescopingListView;
}
abstract boolean dealTouchEvent(boolean topArrived, boolean bottomArrived, MotionEvent ev);
/**
* 给ScrollerTelescopTool用的,配合Scroller使用
*/
void computeScroll() {
}
/**
* 把实际的距离*阻尼参数,增加阻尼效果
*
* @param delTaY
* @return
*/
protected float adjustDelta(float delTaY) {
return delTaY * DAMPING;
}
/**
* 当listview onLayout时候,这时候可以重新获取listView的布局参数
*/
public void listViewLayoutChange() {
}
}
/**
* 用Scroller实现
*/
private static class ScrollerTelescopTool extends TelescopTool {
private Scroller mmScroller;
public ScrollerTelescopTool(TelescopingListView telescopingListView) {
super(telescopingListView);
mmScroller = new Scroller(telescopingListView.getContext());
}
@Override
boolean dealTouchEvent(boolean topArrived, boolean bottomArrived, MotionEvent ev) {
if (mmScroller != null) {
mmScroller.forceFinished(true);
}
float nowRawY = ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mmDeltaYSum = 0;
mmLastRawY = nowRawY;
break;
case MotionEvent.ACTION_MOVE:
float orignalDeltaY = nowRawY - mmLastRawY;
mmDeltaYSum += Math.abs(orignalDeltaY);
float delTaY = adjustDelta(orignalDeltaY);
mmLastRawY = nowRawY;
float nowScrollY = telescopingListView.getScrollY();
if (topArrived) {
if (delTaY > 0) {
telescopingListView.scrollBy(0, (int) -delTaY);
return true;
} else if (delTaY < 0) {
if (nowScrollY < 0) {
telescopingListView.scrollBy(0, (int) -delTaY);
return true;
}
} else {
return true;
}
} else if (bottomArrived) {
if (delTaY < 0) {
telescopingListView.scrollBy(0, (int) -delTaY);
return true;
} else if (delTaY > 0) {
if (nowScrollY > 0) {
telescopingListView.scrollBy(0, (int) -delTaY);
return true;
}
} else {
return true;
}
}
break;
//如果是顶端或者底端,抬起手指时如果在这次触摸过程中滑动了一段距离,
// 那么就将这次事件手动设置为MotionEvent.ACTION_CANCEL,
// 否则将会触发OnitemClick事件
case MotionEvent.ACTION_UP:
smoothScrollTo(0);
if (topArrived || bottomArrived) {
if (mmDeltaYSum >= MINSCROLLDISTANCE) {
ev.setAction(MotionEvent.ACTION_CANCEL);
}
}
break;
}
return false;
}
@Override
void computeScroll() {
if (mmScroller.computeScrollOffset()) {
telescopingListView.scrollTo(mmScroller.getCurrX(), mmScroller.getCurrY());
telescopingListView.invalidate();
}
}
/**
* 工具方法
* 平滑的滑动到一个指定的坐标
*
* @param distnationY y方向要滑动到的目的坐标
*/
private void smoothScrollTo(int distnationY) {
mmScroller.startScroll(telescopingListView.getScrollX(), telescopingListView.getScrollY(), telescopingListView.getScrollX(), distnationY - telescopingListView.getScrollY(), 200);
telescopingListView.invalidate();
}
}
/**
* 用MarginLayoutParams实现
*/
private static class MarginTelescopTool extends TelescopTool {
private MarginLayoutParams mmMarginParams;
public MarginTelescopTool(TelescopingListView telescopingListView) {
super(telescopingListView);
}
@Override
public void listViewLayoutChange() {
mmMarginParams = (MarginLayoutParams) telescopingListView.getLayoutParams();
}
@Override
boolean dealTouchEvent(boolean topArrived, boolean bottomArrived, MotionEvent ev) {
float nowRawY = ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mmLastRawY = nowRawY;
mmDeltaYSum = 0;
break;
case MotionEvent.ACTION_MOVE:
float orignalDeltaY = nowRawY - mmLastRawY;
mmDeltaYSum += Math.abs(orignalDeltaY);
float delTaY = adjustDelta(orignalDeltaY);
mmLastRawY = nowRawY;
if (topArrived) {
float nowTopMargin = mmMarginParams.topMargin;
if (delTaY > 0) {
mmMarginParams.topMargin += delTaY;
listRequestLayout();
return true;
} else if (delTaY < 0) {
if (nowTopMargin > 0) {
mmMarginParams.topMargin += delTaY;
listRequestLayout();
return true;
}
} else {
return true;
}
} else if (bottomArrived) {
if (delTaY < 0) {
mmMarginParams.topMargin += delTaY;
mmMarginParams.bottomMargin -= delTaY;
listRequestLayout();
return true;
} else if (delTaY > 0) {
if (mmMarginParams.bottomMargin > 0) {
mmMarginParams.bottomMargin -= delTaY;
if (mmMarginParams.topMargin < 0) {
mmMarginParams.topMargin += delTaY;
mmMarginParams.topMargin = mmMarginParams.topMargin <= 0 ? mmMarginParams.topMargin : 0;
}
listRequestLayout();
return true;
}
} else {
return true;
}
}
break;
case MotionEvent.ACTION_UP:
mmMarginParams.topMargin = 0;
mmMarginParams.bottomMargin = 0;
listRequestLayout();
if (topArrived || bottomArrived) {
if (mmDeltaYSum >= MINSCROLLDISTANCE) {
ev.setAction(MotionEvent.ACTION_CANCEL);
}
}
break;
}
return false;
}
private void listRequestLayout() {
telescopingListView.setLayoutParams(mmMarginParams);
}
}
}
网友评论