QQ侧滑菜单效果

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;

猜你喜欢

转载自blog.csdn.net/a94721990/article/details/81050647