当下从QQ主界面出现Item侧滑菜单出现之后,很多公司的项目都需要添加这种让人感觉炫酷的功能,但是,目前android API中根本没有提供这种实现,因此就要用自定义的方法来实现该功能。本篇文章实现的基本思路如下:
1.自定义ListView,获取当前滑动的View。
2.自定义LinearLayout充当Item根布局。
3.ListView中按下后,事件交由自定义LinearLayout处理。
三 具体实现
@Override
public boolean onTouchEvent(MotionEvent ev)
{
switch (ev.getAction())
{
case MotionEvent.ACTION_DOWN:
int x = (int) ev.getX();
int y = (int) ev.getY();
int position = pointToPosition(x,y);
if (position != INVALID_POSITION)
{
mSwipeMenuLayout = (SwipeMenuLayout) getChildAt(position - getFirstVisiblePosition());
}
break;
}
if (mSwipeMenuLayout != null)
{
mSwipeMenuLayout.onTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
这些代码相对比较简单,没有什么复杂的逻辑,比较关键的是获取用户按下的点,
用ListView的pointToPosition方法,传入获取的x,y坐标得到当下点击
的Item的position,重点说一下 ListView的getChildAt(int index)方法,
因为ListView与GridView的View复用机制,这个方法默认得到的是当前可见
区域(列表可滚动)的子项!也就表示它的取值范围是在
>= ListView.getFirstVisiblePosition() &&
<= ListView.getLastVisiblePosition(); 因此在程序中获取当前
点击Item View所复用的是哪个Item的View 就需要用
position(当前通过xy坐标获取到的) -
(减去) ListView.getFirstVisiblePosition()
得到的即是复用的Item的position,感兴趣的话,自己可以尝试着看看。
2. 自定义LinearLayout重写其onTouchEvent方法:
/**
* Created by admin on 2016/1/20.
*/
public class SwipeMenuLayout extends LinearLayout
{
/**
* 关闭
*/
public static final int SLIDE_STATUS_OFF = 0;
/**
* 开始滚动
*/
public static final int SLIDE_STATUS_START_SCROLL = 1;
/**
* 已经打开
*/
public static final int SLIDE_STATUS_ON = 2;
private static final int TAN = 2;
/**
* 滚动类
*/
private Scroller mScroller;
/**
* 屏幕尺寸类
*/
private int[] screenSize;
/**
* 主内容区域
*/
private ViewGroup mContent;
/**
* 菜单区域
*/
private ViewGroup mMenu;
/**
* 默认的宽度
*/
private int mMenuWidth = 120;
private int lastX,lastY;
public SwipeMenuLayout(Context context)
{
this(context,null);
}
public SwipeMenuLayout(Context context, AttributeSet attrs)
{
this(context, attrs,0);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context)
{
screenSize = ScreenUtil.getScreenSize(context);
mMenuWidth = ConvertUtils.dp2px(context,mMenuWidth);
this.setOrientation(LinearLayout.HORIZONTAL);
mScroller = new Scroller(context);
}
boolean isOnce = false;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (!isOnce)
{
mContent = (ViewGroup) getChildAt(0);
mContent.getLayoutParams().width = screenSize[0];
mMenu = (ViewGroup) getChildAt(1);
mMenu.getLayoutParams().width = mMenuWidth;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 设置默认的显示
*/
public void shrink()
{
Log.e("----------",getScrollX() + "");
if (getScrollX() != 0)
{
smoothScrollTo(0, 0);
}
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
/**
* 重置上次滑动的View
*/
if (onSlidingListener != null) {
onSlidingListener.onSliding(this, SLIDE_STATUS_START_SCROLL);
}
int x = (int) event.getX();
int y = (int) event.getY();
int scrollX = getScrollX();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
{
/**
* 如果没有结束则停止动画
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE:
{
int deltaX = x - lastX;
int deltaY = y - lastY;
if (Math.abs(deltaX) < Math.abs(deltaY) * TAN) {
break;
}
int newScrollX = scrollX - deltaX;
if (deltaX != 0) {
if (newScrollX < 0) {
newScrollX = 0;
} else if (newScrollX > mMenuWidth) {
newScrollX = mMenuWidth;
}
this.scrollTo(newScrollX, 0);
}
break;
}
case MotionEvent.ACTION_UP:
{
int mX = 0;
if (scrollX - mMenuWidth * 0.75f > 0) {
mX = mMenuWidth;
}
this.smoothScrollTo(mX, 0);
if (onSlidingListener != null) {
onSlidingListener.onSliding(this, mX == 0 ? SLIDE_STATUS_OFF : SLIDE_STATUS_ON);
}
break;
}
}
lastX = x;
lastY = y;
return super.onTouchEvent(event);
}
/**
* 换换滚动到的位置
* @param x
* @param y
*/
private void smoothScrollTo(int x, int y)
{
int scrollX = getScrollX();
int dealtX = x - scrollX;
/**
* 1.startX
* 2.startY
* 3.endX
* 4.endY
* 5.duration
*/
mScroller.startScroll(scrollX,0,dealtX,0,Math.abs(dealtX) * 3);
invalidate();
}
@Override
public void computeScroll()
{
if (mScroller.computeScrollOffset())
{
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void setOnSlidingListener(OnSlidingListener onSlidingListener)
{
this.onSlidingListener = onSlidingListener;
}
private OnSlidingListener onSlidingListener;
public interface OnSlidingListener
{
void onSliding(View view, int state);
}
}
上面SwipeMenuLayout 这个类中
(1) init(context)方法中初始化获取了屏幕的宽高值,滑动出的菜单的宽度设置,在这里只是为了展示效果我设置了一个固定的值,若是你有兴趣,可以尝试调用菜单区域的OnMeasure方法获取设置的值。
(2) OnMeasure设置默认显示方式。
(3) onTouchEvent方法中:首先 发送了一个回调事件,目的是还原上次打开的Item
ACTION_DOWN:若是上次的还未滚动完则终止上次的
ACTION_MOVE: 添加了Item范围的控制,其中scrollX (当前view的左上角相对于母视图的左上角的X轴偏移量) ,deltaX 将要滚动的值 (本次滑动的最后位置,减去上次的位置余下的需要滑动的值),x(本次滑动停留的位置),lastX(保留的上次滑动的最后位置)
ACTION_UP:scrollX (当前view的左上角相对于母视图的左上角的X轴偏移量) 如果这个偏移量的值大于了Menu视图的0.75也就是3/4那么就显示菜单(这个值可以自己设置),发送回调通知菜单展示。
(4) smoothScrollTo(int x,int y)在这个方法中主要说一下 startScroll(startX,startY,endX,endY,duration) startX:起始X轴滚动位置 startY:起始Y轴滚动位置
endX:结束X轴的位置 endY:起始Y轴的位置 duration:间隔时间
那么该类中还有一个方法没有讲:
@Override
public void computeScroll()
{
if (mScroller.computeScrollOffset())
{
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
这个方法是干什么的呢?当我们执行ontouch或invalidate()或postInvalidate()都会导致这个方法的执行 ,因此我们在这个方法中判断了mScroller的startScroll()方法是否执行完毕,若是完成的话,则滚动到最终位置,并重新绘制界面。
3.事件传递
这个就更简单啦,因为我们重写了ItemView的OnTouchEvent方法,因此在ListView中我们做了引用SwipeMenuLayout来对应当前滑动的Item的View 并调用该Item View的onTouchEvent()传入当前的点击事件,就完成了事件的传递。
4.后记
因为这个代码是第一个完成的初品版本,因此可能在代码中还有没有测到的bug,后面会有源码奉上,若是你在使用的过程中,发现有问题,或者有更好的思路,欢迎你在留言区留言,谢谢。
至此 这个简单的Item侧滑就实现了。代码中没有太多比较难理解的地方,只要我们能稍稍多想一点,了解View的绘制已经生命周期原理。建议大家没事多练练 多看些思路比较清晰的大神的blog 相信你会有所收获:
大神爱哥博客传送
http://my.csdn.net/aigestudio
尤其喜欢他的自定义控件系列
大神郭霖博客传送
http://my.csdn.net/sinyu890807
大神鸿洋博客传送
http://my.csdn.net/lmj623565791
源码下载