QQ侧滑菜单效果
写一个SlideMenu类,继承自FrameLayout,因为如果继承自ViewGroup的话,需要我们自己来实现onMeasure方法,而该方法的实现一般比较麻烦且没有必要,所以选择继承系统的已有的控件FrameLayout,不用其他控件是因为FrameLayout最轻量级
在布局文件中给SlideMenu添加2个子布局,分别是菜单的布局和主界面的布局(代码略);
移动View的方法总结:
通过改变View的scroll的坐标来移动:
scrollTo(x,y);//滚动到指定位置
scrollBy(xOffset,yOffset);//滚动多少距离
通过改变View在父View中的布局的位置:
offsetLeftAndRight(offset);//同时更改view的left和right
offsetTopAndBottom(offset);//同时更改view的top和bottom
layout(l,t,r,b);
但是谷歌发现很多View移动的情景有相识点, 所以封装了ViewDragHelper类来帮助我们在ViewGroup中进行子View的移动:
ViewDragHelper类的介绍
谷歌在2013年I/O开发者大会上提出;
专门用于在ViewGroup中对子View进行拖拽处理;
在19(Android4.4)以及以上的v4包中;
本质是封装了对触摸事件的解析,包括触摸位置,触摸速度以及Scroller的封装,只需要我们在回调方法中指定是否移动,移动多少等等,但是需要注意的是:它只是一个触摸事件的解析类(如GestureDecetor),所以需要我们传递给它触摸事件,它才能工作;
如何创建ViewDragHelper对象
ViewDragHelper viewDragHelper = ViewDragHelper.create(this, callback);
由于ViewDragHelper只是触摸事件解析类,要想让ViewDragHelper工作,需要将触摸事件传递给它
public boolean onInterceptTouchEvent(MotionEvent ev) {
//让ViewDragHelper帮助我们判断是否应该拦截
boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper来解析
viewDragHelper.processTouchEvent(event);
return true;
}
在onFinishInflate方法中初始化子View的引用
protected void onFinishInflate() {
super.onFinishInflate();
menuView = getChildAt(0);
mainView = getChildAt(1);
}
在onSizeChanged方法中初始化宽高,因为该方法在onMeasure之后执行
int menuWidth = menuView.getMeasuredWidth();
int mainWidth = mainView.getMeasuredWidth();
float dragRange = mainWidth * DRAG_RANGE_FRACTION;
实现tryCaptureView方法来判断要对哪个View进行触摸事件的捕获
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child==mainView || child==menuView;
}
实现getViewHorizontalDragRange方法,该方法必须要实现,返回的值自己来定义 ,只要大于0就行,否则在某些情况下不能水平滑动
public int getViewHorizontalDragRange(View child) {
return (int) dragRange;
}
重写clampViewPositionHorizontal方法,控制子View在水平方向上的移动
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(child==mainView){
//对mainView的移动进行限制了
if(left>dragRange){
left = (int) dragRange;
}
if(left<0){
left = 0;
}
}
return left;
}
10.在onViewPositionChanged方法中实现一些伴随移动的效果,因为在该方法中可以获取view移动的距离
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//如果当前移动的是menuView,则让mainView跟随移动
if(changedView==menuView){
//固定住menuView,不让它动
menuView.layout(0, 0,menuWidth, menuHeight);
//手动让mainView进行伴随移动
int newLeft = mainView.getLeft()+dx;
//对newLeft进行限制
if(newLeft>dragRange){
//限制右边
newLeft = (int) dragRange;
}
if(newLeft<0){
newLeft = 0;//限制左边
}
mainView.layout(newLeft, mainView.getTop(),newLeft+mainWidth,
mainView.getBottom());
}
}
在onViewReleased方法中处理手指抬起的缓慢移动
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(mainView.getLeft()>dragRange/2){
//open
viewDragHelper.smoothSlideViewTo(mainView, (int) dragRange, mainView.getTop());
ViewCompat.postInvalidateOnAnimation(this);
}else {
//close
viewDragHelper.smoothSlideViewTo(mainView, 0, mainView.getTop());
ViewCompat.postInvalidateOnAnimation(this);
}
};
同时重写computeScroll方法:
@Override
public void computeScroll() {
super.computeScroll();
if(viewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
在onViewPositionChanged方法中根据当前View移动的百分比实现伴随动画效果
//1.计算mainView滑动的百分比
float fraction = mainView.getLeft()/dragRange;
//2.根据滑动的百分比的值,去执行伴随的动画
executeAnim(fraction);
而executeAnim方法的实现如下:
private void executeDragAnim(float dragFraction){
//dragFraction : 0 - 1
//scale : 1 - 0.8
//float scaleValue = 0.8f + (1-dragFraction)*0.2f;
//缩小mainView
ViewHelper.setScaleX(mainView, floatEvaluator.evaluate(dragFraction, 1f, 0.8f));
ViewHelper.setScaleY(mainView, floatEvaluator.evaluate(dragFraction, 1f, 0.8f));
//放大并移动menuVIew
ViewHelper.setScaleX(menuView, floatEvaluator.evaluate(dragFraction, 0.5f, 1f));
ViewHelper.setScaleY(menuView, floatEvaluator.evaluate(dragFraction, 0.5f, 1f));
ViewHelper.setTranslationX(menuView, floatEvaluator.evaluate(dragFraction, -menuWidth/2, 0));
ViewHelper.setAlpha(menuView, floatEvaluator.evaluate(dragFraction, 0.2f, 1f));
//改变SlideMenu的背景亮度
getBackground().setColorFilter((Integer) ColorUtil.evaluateColor(dragFraction, Color.BLACK, Color.TRANSPARENT), Mode.SRC_OVER);
}
定义接口回调,将SlideMenu打开,滑动和关闭的事件暴漏给外界
private OnSwipeListener listener;
public void setOnSwipeListener(OnSwipeListener listener){
this.listener = listener;
}
public interface OnSwipeListener{
/**
* 打开的回调
*/
void onOpen();
/**
* 关闭的回调
*/
void onClose();
/**
* 拖拽过程中的回调
*/
void onDraging(float fraction);
}
在MainActivity中给SlideMenu设置OnSwipeListener,并在接口的回调方法中去执行一些简单逻辑:
slideMenu.setOnDragStatusChangeListener(new OnDragStatusChangeListener() {
@Override
public void onOpen() {
Random random = new Random();
menuListView.smoothScrollToPosition(random.nextInt(Constant.sCheeseStrings.length));
}
@Override
public void onDragging(float dragFraction) {
ViewHelper.setAlpha(iv_head, 1-dragFraction);
}
@Override
public void onClose() {
ViewPropertyAnimator.animate(iv_head).translationX(40).setDuration(600).setInterpolator(new CycleInterpolator(4));
}
});
滑动删除效果
先写一个类SwipeLayout,继承自FrameLayout
在onLayout方法中对contentView和deleteView进行摆放:
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
contentView.layout(0, 0, contentWidth, contentHeight);
deleteView.layout(contentWidth, 0, contentWidth+deleteWidth, deleteHeight);
}
结合上午所学知识,利用ViewDragHelper实现让SwipeLayout的2个子View进行拖拽移动,主要是Callback的实现,如下:
private ViewDragHelper.Callback callback = new Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child==contentView || child==deleteView;
}
public int getViewHorizontalDragRange(View child) {
return deleteWidth;
}
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(child==contentView){
//限定contentView
if(left>0)left = 0;
if(left<-deleteWidth)left = -deleteWidth;
}else if (child==deleteView) {
//限定deleteView
if(left>contentWidth)left = contentWidth;
if(left<(contentWidth-deleteWidth)){
left = contentWidth-deleteWidth;
}
}
return left;
}
/**
* 一般实现view的伴随移动
*/
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//如果contentView移动了,那么让deleteView伴随移动
if(changedView==contentView){
int newLeft = deleteView.getLeft()+dx;
deleteView.layout(newLeft,0, newLeft+deleteWidth,deleteHeight);
}else if (changedView==deleteView) {
//让contentView做伴随移动
int newLeft = contentView.getLeft()+dx;
contentView.layout(newLeft,0, newLeft+contentWidth,contentHeight);
}
};
/**
* 松开手指回调
*/
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if(contentView.getLeft()<-deleteWidth/2){
//open
open();
}else {
//close
close();
}
};
};
然后将实现好的可滑动的SwipeLayout放入ListView的adapter的布局中,此时我们遇到2个bug:
当我们左右拖动item滑动时,再上下滑动会遇到事件被ListView捕获并处理,导致我们无法继续控制item的滑动;
我们可以同时滑动出多个item,而需求是只能允许一个item是打开的;
解决第一个bug的思路是:在onTouchEvent方法判断当前手指移动的方向到底是偏向于水平还是偏向于垂直,如果是偏向于水平那么就认为用户是希望滑动item,那么则请求父View不要去拦截事件
private float downX,downY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float moveY = event.getY();
//计算移动的距离
float deltaX = moveX - downX;
float deltaY = moveY - downY;
//判断手指移动的方向到底是偏向于水平还是垂直
if(Math.abs(deltaX)>Math.abs(deltaY)){
//说明偏向于水平,那么认为要滑动条目,则listview不应该拦截
requestDisallowInterceptTouchEvent(true);//请求父VIew不拦截
}
break;
case MotionEvent.ACTION_UP:
break;
}
viewDragHelper.processTouchEvent(event);
return true;
}
定义滑动监听器:
private OnSwipeListener onSwipeListener;
public OnSwipeListener getOnSwipeListener() {
return onSwipeListener;
}
public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
this.onSwipeListener = onSwipeListener;
}
public interface OnSwipeListener{
void onOpen();
void onClose();
}
在Activity的adapter中设置监听器:
//设置监听器
holder.swipeLayout.setOnSwipeListener(new SwipeLayout.OnSwipeListener() {
@Override
public void onOpen(SwipeLayout swipeLayout) {
if(openLayout!=null && openLayout!=swipeLayout){
openLayout.close();
}
openLayout = swipeLayout;
}
@Override
public void onClose(SwipeLayout swipeLayout) {
if(openLayout==swipeLayout){
openLayout = null;
}
}
});
最后,由于我们重写onTouchEvent处理了事件,导致ListView的条目点击无效了,此时最有效最简单的做法是自己去判断触摸事件实现点击行为,思路是:记录按下的坐标和时间,在抬起的时候计算整个按下抬起的时间和距离,如果时间小于400毫秒,并且距离小于touchSlop,则认为是点击事件,事实上系统也是这样实现点击事件的:
case MotionEvent.ACTIONDOWN:
downX = event.getX();
downY = event.getY();
//记录按下的时间点
downTime = System.currentTimeMillis();
break;
case MotionEvent.ACTIONUP:
//计算抬起的时间点
long upTime = System.currentTimeMillis();
//计算抬起的坐标
float upX = event.getX();
float upY = event.getY();
//计算按下和抬起的总时间
long touchDuration = upTime-downTime;
//计算按下点和抬起点的距离
float touchD = Utils.getDistanceBetween2Points(new PointF(downX, downY), new PointF(upX, upY));
if(touchDuration<400 && touchD<touchSlop){
if(listener!=null){
listener.onItemClick();
}
}
break;