自定义View之QQ侧滑删除
作者:Noblel
链接:http://www.jianshu.com/p/b81cae309594
请尊重原创,谢谢!
需求来源:
这是某一个版本的,现在的版本不是这样的了。
实现思路
1.分析元素和效果:
有两块布局,一个是内容,一个是菜单布局,可以左滑而且根据位置自动关闭和打开。
2.自定义属性:
为了简单,这里我就没有写自定义属性,套路都一样。不过思路上要记得这一步。
3.编写SlideView
3.1. 选择合适的布局,这里直接extends最简单的FrameLayout
3.2. 定义变量和初始化布局
原理图/**
* 内容视图
*/
private View mContentView;
/**
* 菜单视图
*/
private View mMenuView;
/**
* 布局文件加载完成回调的方法
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentView = getChildAt(0);
mMenuView = getChildAt(1);
}
item_main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.noblel.slideview.SlideLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<include
android:id="@+id/item_content"
layout="@layout/item_content" />
<include
android:id="@+id/item_menu"
layout="@layout/item_menu" />
</com.noblel.slideview.SlideLayout>
item_content我们直接放一个TextView,menu中放三个TextView。
item_content.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#ffffff"
android:gravity="center"
android:textColor="#000000"
android:textSize="25sp"/>
item_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:background="#ffffff"
android:layout_height="60dp">
<TextView
android:id="@+id/tv_top"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="#C7C7CD"
android:gravity="center"
android:padding="5dp"
android:text="置顶"
android:textColor="#ffffff"
android:textSize="25sp" />
<TextView
android:id="@+id/tv_no_read"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_weight="1"
android:background="#FF9D01"
android:gravity="center"
android:padding="5dp"
android:text="标为未读"
android:textColor="#ffffff"
android:textSize="25sp" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="#FF3A30"
android:gravity="center"
android:padding="5dp"
android:text="删除"
android:textColor="#ffffff"
android:textSize="25sp" />
</LinearLayout>
3.3. 有两部分视图那么我们需要在onLayout中给他们安排座位。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取内容的宽度
mContentWidth = mContentView.getMeasuredWidth();
//获取菜单视图的宽度
mMenuWidth = mMenuView.getMeasuredWidth();
//获取内容高度
mContentHeight = mContentView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//指定菜单位置
mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mContentHeight);
}
3.4. 设置侧滑系列操作
-
在左滑的时候拦截事件交给自己onTouchEvent处理
@Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercept = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //记录按下坐标 mDownX = mStartX = event.getX(); mDownY = event.getY(); if (mOnStateChangeListener != null) { mOnStateChangeListener.onDown(this); } break; case MotionEvent.ACTION_MOVE: float endX = event.getX(); //重新赋值 mStartX = event.getX(); //在X轴滑动的距离 float DX = Math.abs(endX - mDownX); //大于一定距离才处理 intercept = DX > 8; break; } return intercept; }
-
计算滑动偏移量,并将视图移动更新视图
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: float endX = event.getX(); float endY = event.getY(); //计算偏移量 float distanceX = endX - mStartX; //getScrollX()和getX()和getRawX()后面做总结 int toScrollX = (int) (getScrollX() - distanceX); if (toScrollX < 0) { toScrollX = 0; } else if (toScrollX > mMenuWidth) { toScrollX = mMenuWidth; } //scrollTo和scrollBy()区别? scrollTo(toScrollX, 0); //重新赋值 mStartX = event.getX(); //在X轴和Y轴滑动的距离 float DX = Math.abs(endX - mDownX); if (DX > 5) { //水平方向滑动响应侧滑,设置反拦截-父控件不拦截,事件交给slideLayout getParent().requestDisallowInterceptTouchEvent(true); } break; case MotionEvent.ACTION_UP: int totalScrollX = getScrollX(); //手离开时候判断滑动距离小于一半时候关闭菜单 if (totalScrollX < mMenuWidth / 2) { closeMenu(); } else { openMenu(); } break; } return true; }
-
关闭菜单(开启菜单类似)
/** * 关闭菜单 */ public void closeMenu() { int distanceX = 0 - getScrollX(); //设置scroller滑动 mScroller.startScroll(getScrollX(), getScrollY(), distanceX, getScrollY()); //强制刷新 invalidate(); if (mOnStateChangeListener != null) { mOnStateChangeListener.onClose(this); } }
-
设置computeScroll()
/** * View在draw()的过程中调用 */ @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } }
3.5 设置相关接口
/**
* 监听slideLayout状态的改变
*/
public interface OnStateChangeListener {
void onClose(SlideLayout layout);
void onDown(SlideLayout layout);
void onOpen(SlideLayout layout);
}
效果:
效果源码
问题思考:
-
ListView多点触控会出现多个item同时打开菜单
解决办法: 设置android:splitMotionEvents="false" -
获取mContentWidth在onLayout的mMenuView.layout()之后会有一个item显示不出来
原因: ListView的复用机制中会多次调用onLayout方法,而在mMenuView.layout()之后值都为0,所以显示不出来。具体需要了解ListView复用机制。
getScrollX()和getX()和getRawX()区别?
getX():触摸点相对于其所在视图原点在x轴上的偏移量
getScrollX():当前视图相对于屏幕原点在x轴上的偏移量,就是实际视图(包括未显示出来的)距离屏幕左边缘的距离。如果在右边则为负数,在左边则为正数。
getRawX():触摸点相对于屏幕原点在x轴上的偏移量,即getRawX() = getX() + getScrollX()
scrollTo和scrollBy()区别?
public void scrollBy(int x, int y) {
//scrollBy就是调用scrollTo()
scrollTo(mScrollX + x, mScrollY + y);
}
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
scrollTo():在当前视图内容偏移至(x , y)坐标处,即显示(可视)区域位于(x , y)坐标处。
scrollBy(): 在当前视图内容继续偏移(x , y)个单位,显示(可视)区域也跟着偏移(x,y)个单位。
computeScroll()是做什么的?
computeScrollOffset()方法会计算出下一个滚动到的位置,通过getCurrX(),getCurrY()这两个方法获得,在没滚到我们设定距离前返回是true,到了我们设定的距离后就返回false,表示计算完成了,滚动也这个时候完成。起始偏移量和移动距离和时间通过startscroll()设置的。他只是个设置的,并会自动绘制,要绘制就通常我们调用viewgroup的invalidate()等相关方法来重绘制。然后viewgroup会调用computeScroll()这个空方法,在这里面不断循环判断computeScrollOffset()和移动scrollTo()来达到我们自动滚动到目标距离。