请尊重个人劳动成果,转载注明出处,谢谢!
http://blog.csdn.net/amazing7/article/details/51768942
先看 SwipeLayout的效果图
图太多了,我这只上传1张,想看 listview和GridView效果的,和想看源码的 —> GitHub
怎样实现后面说,先说会空话。
最近整理之前的项目,那时有1个这样的需求,要在ExpandableListView的ChildView上实现 编辑和删除的侧滑菜单。我当时并没有用他人的框架,实现出来大概是这个模样。
滑动事件完好,没有冲突,子view可以取得点击事件。
实现起来非常简单,大概分为3步:
①、这个Item布局采取HorizontalScrollView来作为最外层布局(对,就是它,它可以横向转动),注意 HorizontalScrollView继承的是FrameLayout,意味着只能有1个子布局。在这个子布局里,摆放两个子子布局,left用于显示内容,right显示菜单。
②、初始化中获得屏幕宽度
DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
mScreentWidth = dm.widthPixels;
在Adapter的getChildView方法中设置leftView的宽度为屏幕宽度
// 设置leftView的大小为屏幕宽度,这样右侧的rightView就正好被挤出屏幕外
holder.leftview= convertView.findViewById(R.id.left);
LayoutParams lp = holder.leftview.getLayoutParams();
lp.width = mScreentWidth;
③、getChildView方法中给convertView设置Touch监听事件
convertView.setOnTouchListener(new View.OnTouchListener()
{
@Override
public boolean onTouch(View v, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
if (view != null) {
//有view被滑开了,点击其他childview时,用于还原
ViewHolder viewHolder = (ViewHolder) view.getTag();
viewHolder.hsv.smoothScrollTo(0, 0);
}
break;
case MotionEvent.ACTION_UP:
ViewHolder viewHolder = (ViewHolder) v.getTag();
view = v;
// 取得HorizontalScrollView滑动的水平方向值.
int scrollX = viewHolder.hsv.getScrollX();
int rightW = viewHolder.rightView.getWidth();
if (scrollX < rightW / 4)
{ //滑动距离小于右侧布局的1/4收缩
viewHolder.hsv.smoothScrollTo(0, 0);
}else
{ //展开
viewHolder.hSView.smoothScrollTo(rightW, 0);
}
break;
}
return true;
}
});
// 删除1条后更新状态
if (holder.hsv.getScrollX() != 0) {
holder.hsv.scrollTo(0, 0);
}
简单说1下,HorizontalScrollView在dispatchTouchEvent的时候,如果发现时横向滑动就把事件交给onTouchEvent处理,而这个onTouchEvent方法是来自view的(viewGroup也是调用view的该方法),在view的dispatchTouchEvent中,是顺序调用OnTouchListener和onTouchEvent的。我们这setOnTouchListener中自己处理了滑动事件,并且返回true,就消耗掉了事件,不会再调用onTouchEvent。
开始说主题,假定都对 view和viewGroup的事件分发机制 、自定义viewGroup 的流程 和 Scroller 都有了初步的了解。如果没有,我们就假定有!
算了,还没有了解可以看看 View和ViewGroup事件分发机制源码分析 和 自定义FlowLayout实现标签快捷输入框 、Android Scroller大揭秘这3篇文章。
我是继承LinearLayout实现的,为何不继承HorizontalScrollView或是ViewGroup?
HorizontalScrollView其实就是1个实现转动功能的FrameLayout,view的onMeasure和onLayout是层层实现的,我不能在HorizontalScrollView的onLayout方法中对其子view的子view直接设置为屏幕宽度。
继承ViewGroup就要自己丈量和摆放子view,开甚么玩笑,我这么懒得人。
为了节省大家宝贵的时间,下面只说重点。
为何有事件冲突呢? 我们知道listview是上下滑动的,而我们的这个侧滑布局要左右滑动。当我们屏幕上横向滑动时,只要略微斜了1点,那末listview就认为要上下滑动,它就把滑动事件拦截了自己交给自己的onTouchEvent处理。根本都分发不到SwipeLayout的局部中。产生效果:侧滑出来1点划不动了,卡住了…
但是google早已看穿了1切,他们给我们提供了这个方法。
requestDisallowInterceptTouchEvent(true);
看过源码的火伴肯定知道,这其实就是设置1个标志位。当传入true时,驳回父view的中断要求。( 就是父view不能中断事件必须分发到子view。)
那末事件就由我们处理了,由于listview需要上下滑动事件,而SwipeLayout需要左右滑动事件,恰好各取所需,复写dispatchTouchEvent(MotionEvent ev)方法来实现各取所需。
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
/**
* 不允许父view对触摸事件的拦截
*/
disallowParentsInterceptTouchEvent(getParent());
startX = ev.getX();
startY = ev.getY();
isHorizontalMove =false;
break;
case MotionEvent.ACTION_MOVE:
if(!isHorizontalMove){
curX = ev.getX();
curY = ev.getY();
float dx = curX - startX;
float dy = curY - startY;
/**
* 认为产生了滑动
*/
if(dx*dx+dy*dy > mTouchSlop*mTouchSlop){
/**
* 垂直滑动
*/
if (Math.abs(dy) > Math.abs(dx)){
/**
* 允许父view对触摸事件拦截,让其他view去处理事件
*/
allowParentsInterceptTouchEvent(getParent());
/**
* 垂直转动复原所有item
*/
shrinkAllView();
}else{
/**
* 水平滑动,拦截来自己处理
*/
isHorizontalMove = true;
/**
* 为了在onTouchEvent的Move事件中第1次摹拟滑动距离不要太大,
* 记录上1次产生move的位置
*/
lastX = curX;
}
}
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
在ACTION_DOWN中 驳回父view拦截,那末我们就能够开心的在ACTION_MOVE对事件进行处理(先判断是不是产生了滑动,再判断是 上下 还是 左右,是上下就取消对父类的驳回,让listview去处理事件,同时让所有划开的SwipeLayout关闭。是左右滑动就 中断继续往下层分发,拦截到自己的onTouchEvent做事件处理)。
中断分发:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(isHorizontalMove){
/**
* 产生水平滑动,把事件中断到本层onTouchEvent中处理
*/
return true;
}
return super.onInterceptTouchEvent(ev);
}
我这侧滑菜单里是两个imageView默许是不可clickable的,所以就算不拦截最后也会履行我的onTouchEvent处理。但是如果是Button,ImageButton等默许是可点击的,那末事件就传不上来了。
由于我们不知道是哪一个父view对事件进行了拦截,所以要循环递归设置标志位(为了更好的兼容性,反正我这是listview拦截了)。
/**
* 由于不知道是父view那1层会拦截触摸事件,所以递归向上设置标志位
* 直到顶层view,就直接返回
*/
private void disallowParentsInterceptTouchEvent(ViewParent parent) {
if (null == parent) {
return;
}
parent.requestDisallowInterceptTouchEvent(true);
disallowParentsInterceptTouchEvent(parent.getParent());
}
private void allowParentsInterceptTouchEvent(ViewParent parent) {
if (null == parent) {
return;
}
parent.requestDisallowInterceptTouchEvent(false);
allowParentsInterceptTouchEvent(parent.getParent());
}
当标志位表明是横向滑动时,我们需要在ACTION_MOVE里面摹拟转动。触发1次ACTION_MOVE就让内容转动1点点,实现效果就是内容跟随手指移动。
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if(isHorizontalMove){
curX = ev.getX();
float dX = curX-lastX;
/**
* 不断更新lastX的位置,用于摹拟滑动
*/
lastX = curX;
/**
* 滑动的距离与实际相反,由于转动的时候移动的是内容,不是view
*/
int disX = getScrollX() + (int)(-dX);
/**
* 手指向右移动
*/
if(disX<0){
/**
* 如果菜单收缩,避免越界(越界后ACTION_UP又会转动回来,但还是不越界的好)
* 如果菜单展开,,我们希望迅速关闭菜单,不需要摹拟转动
*/
scrollTo(0, 0);
}
/**
* 手指向左移动,如果累加的移动距离已大于menu的宽度,就让menu显示出来。
* 如果移动距离还不到,就摹拟转动
*/
else if(disX>rightViewWidth){
scrollTo(rightViewWidth,0);
}
else{
scrollTo(disX, 0);
}
}
break;
case MotionEvent.ACTION_UP:
float endX = ev.getX();
float dis =endX -startX;
/**
* 手指向左滑动,摹拟展开
*/
if(dis<0){
SimulateScroll(EXPAND);
}
/**
* 手指向右滑动,摹拟关闭
*/
else{
SimulateScroll(SHRINK);
}
default:
break;
}
return true;
}
先说1下这个lastX,这个坐标是onInterceptTouchEvent中判断为横向移动后的最近坐标。(事件不能被消费掉,但是会随着时间消失,我们可以在最内层到最外层的所有view的onTouchEvent中对1个事件进行处理,全部返回false。但是这里是 在onInterceptTouchEvent中已触发了11个(大概)ACTION_MOVE事件,然后onTouchEvent才进行处理。简单的说就是我滑动的前1段距离拿去做判断了,判断好了才跟这手指移动。)
那这么办呢?要末
float dX = curX-lastX;
滑动顺畅,不足的距离由最后scroller摹拟转动补回。
float dX = curX-startX;
刚产生移动那1下移动约10个ACTION_MOVE事件的距离,效果不好。
给不了解的恶补1下概念:
getX() 和getY()是view的内部坐标,大小补回超太长宽。
getScrollX()
获得的是view左上角到内容左上角的距离。至于正负表示方向。
Positive numbers will scroll the content to the left.
屏幕刚显示,未产生移动之前getScrollX()等于0,每次scrollTo后会刷新getScrollX()的值。
在1次ACTION_MOVE事件中,手指移动了dx距离,那末让内容也1起移动dx,就实现了跟随手指移动的效果。
相当于 getScrollX()+=dx。
当我们手指拿起来的时候,要判断SwipeLayout的菜单栏是应当收缩还是应当展开。当肯定了状态后就要摹拟手指触摸滑动到指定位置。
/**
* move事件里摹拟滑动完成后,判断展开状态
* 再摹拟转动到目标位置
*/
public void SimulateScroll(int type){
int dx =0;
switch (type){
case EXPAND:
//手指向左滑动getScrollX为正
dx = rightViewWidth-getScrollX();
break;
case SHRINK:
//手指向右滑动getScrollX为负
dx = 0-getScrollX();
break;
default:
break;
}
scroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx)/2);
invalidate();
}
@Override
public void computeScroll() {
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*
* 返回true代表正在摹拟数据,false 已停止摹拟数据
*/
if (scroller.computeScrollOffset()) {
/**
* 更新X轴的偏移量
*/
scrollTo(scroller.getCurrX(), 0);
/**
* 递归调用computeScroll()方法,直到摹拟转动完成
*/
invalidate();
}
}
这里调用invalidate()重绘UI,会再次调用computeScroll(),递归 直到 摹拟转动完成。
listview滑动时所有SwipeLayout复原。
我们知道listview删除1个item,不1定会回收view,有可能只是重新装载了数据。那末显示的时候要SwipeLayout复原。
/**
* 用于上下滑动和删除item时的,状态改变
*/
static List<SwipeLayout> swipelayouts = new ArrayList<>();
public static void addSwipeView(SwipeLayout v){
if(null==v){
return;
}
swipelayouts.add(v);
}
public static void removeSwipeView(SwipeLayout v){
if(null==v){
return;
}
v.SimulateScroll(SwipeLayout.SHRINK);
}
private void shrinkAllView(){
for(SwipeLayout s :swipelayouts){
if(null==s){
swipelayouts.remove(s);
continue;
}else {
s.SimulateScroll(SwipeLayout.SHRINK);
}
}
}
内部定义了3个方法,当删除item时调用removeSwipeView方法使该view复原。在adapte的getview方法添加新item时调用addSwipeView,把当前显示的所有SwipeLayout都装在这个list里面。在上面dispatchTouchEvent中判断为上下滑动事件的时候调用shrinkAllView复原所有。
基本上就完了,有木有很简单!哈哈
我们这里是解决了上下滑动和左右滑动的冲突,那末listview 中item为scrollview时,两个都要竖着滑动。
我们为何想要在listview 中使用scrollview,由于我们的item中需要显示的太多。我们知道当scrollview里的内容高度 小于它父view给他的高度的时候,它是完全展开的,不需要也不能滑动。那末使listview给item足够大的高度,让scrollview没必要以转动的方式来展现,就能够解决这个滑动冲突,反正转动事件也会被listview 拦截。
新建1个新的NoScrollListView继承与listview,并复写onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这好比你有1个炒鸡有钱的爹,你想要多少都能满足你,对! 是满足你...
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 7, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
那末我有1个问题,viewpage+fragment+listview +SwipeLayout,我们知道viewpage要左右滑动,当产生滑动事件的时候,我是该移动SwipeLayout呢?还是让viewpage子去翻页呢?
下一篇 多线程下载文件