QQ侧滑面板特效的实现

ViewDragHelper的介绍

  要实现和QQ5.0侧滑的特效,需要借助谷歌在2013年I/O大会上发布的ViewDragHelper类,提供这个类目的就是为了解决拖拽滑动问题。
  使用v4包中的ViewDragHelper为了兼容低版本,所以在创建ViewDragHelper对象时如果找不到ViewDragHelper这个类,可以从sdk中拷贝出最新的v4包覆盖lib目录中的V4包即可。

  • 效果图:

效果图

/**
 * Created by Alan on 2016/8/18.
 * QQ5.0侧滑菜单特效实现
 */

 //自定义一个类继承FrameLayout
public class DragLayout extends FrameLayout {

/**
 * ViewDragHelper:
 * v4包中的api,2013年谷歌I/O大会上提出,用于解决控件拖动问题。
 */
   private ViewDragHelper mDragHelper;
   private View mMenuView;//菜单
   private View mMainView;//主界面
   private int mWidth;//控件宽度
   private int mHeight;///控件高度
   private int mMRange;//侧拉菜单打开的最大宽度

   public DragLayout(Context context) {
       super(context);
       init();
   }

   public DragLayout(Context context, AttributeSet attrs) {
       super(context, attrs);
       init();
   }

   public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       init();
   }

   //4.处理Callback接口中的回调方法
   ViewDragHelper.Callback mCallack = new ViewDragHelper.Callback() {
       /**
        * 根据返回值决定子控件是否可以拖动, true表示可以拖动
        * @param child 被捕获的子控件
        * @param pointerId
        * @return
        */
       @Override
       public boolean tryCaptureView(View child, int pointerId) {

           return true;
       }

       /**当开始拖动时回调*/
       @Override
       public void onViewCaptured(View capturedChild, int activePointerId) {
           System.out.println("onViewCaptured -- 拖动");
       }

       /**
        * 根据返回值决定子控件将要显示的位置
        * @param child
        * @param left
        * @param dx
        * @return
        */
       @Override
       public int clampViewPositionHorizontal(View child, int left, int dx) {
           if (child == mMainView) {

               left = fixDragLeft(left);
           }
           return left;
       }

       /**
        * 获取水平方向拖动的最大范围
        * 注意:此方法半不会真正限制子控件的滑动范围,
        * 如果要实现拖动效果,应该返回大于0的值
        * @param child
        * @return mMRange 范围
        */
       @Override
       public int getViewHorizontalDragRange(View child) {
           return mMRange;
       }

       /**
        * 当拖动结束松开手时回调, 需要在此方法中设置菜单为打开或关闭状态
        * 侧滑菜单何时打开和关闭?
        * 打开的情况:
     *   1) 滑动速度大于0
        *   2)  速度是0且位置大于拖拽范围的一半
        * 关闭的情况:其他的情况都是关闭
        * 
        * @param releasedChild
        * @param xvel 松开手时水平方向的速度,像素/秒,往右为正
        * @param yvel
        */
       @Override
       public void onViewReleased(View releasedChild, float xvel, float yvel) {
           super.onViewReleased(releasedChild, xvel, yvel);
           // 松开手时打开或关闭侧滑菜单
           if (xvel > 0) {
               open();
           } else if (xvel == 0 && mMainView.getLeft() > mMRange / 2) {
               open();
           } else {
               close();
           }

       }

       /**
        * 当前界面发生改变回调
        * 处理逻辑: 关联滑动, 事件监听, 伴随动画
        * @param changedView
        * @param left
        * @param top
        * @param dx
        * @param dy
        */
       @Override
       public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
           //  super.onViewPositionChanged(changedView, left, top, dx, dy);
           // 当滑动菜单时,同时滑动主界面
           if (changedView == mMenuView) {
               // 保持菜单界面位置不变
               mMenuView.layout(0, 0, mWidth, mHeight);

               // 关联滑动,菜单滑动多少,主界面也滑动多少
               int newLeft = mMainView.getLeft() + dx;
               newLeft = fixDragLeft(newLeft);
               mMainView.layout(newLeft, 0, newLeft + mWidth, mHeight);
           }
           // 监听打开关闭拖动的状态
           listenDragState();
           // 伴随动画
           animateChildren();

       }
   };
 /**
    * 伴随动画
    */
   private void animateChildren() {
       float percent = ((float) mMainView.getLeft()) / mMRange;

       //菜单界面
       // 平移: [-mWidth / 2, 0]
       // 透明度变化: [0.4, 1]
       // 缩放: [0.5f, 1]
       mMenuView.setTranslationX(evaluate(-mWidth/2,0,percent));
       mMenuView.setAlpha(evaluate(0.4f, 1, percent));
       mMenuView.setScaleX(evaluate(0.5f, 1f, percent));
       mMenuView.setScaleY(evaluate(0.5f, 1f, percent));

       // 主界面:  缩放[1, 0.8f]
       mMainView.setScaleX(evaluate(1f, 0.8f, percent));
       mMainView.setScaleY(evaluate(1f, 0.8f, percent));

       Drawable drawable = getBackground();
       if (drawable != null) {
           int color = (int) evaluateColor(percent, Color.BLACK, Color.TRANSPARENT);
           drawable.setColorFilter(color , PorterDuff.Mode.SRC_OVER);
       }
   }
   /**
    * 估值器
    */
   public float evaluate(float start,float end,float percent){
       // 中间值 = 开始值 + (结束值 - 开始值) * 百分比
       return start+(end - start)*percent;
   }
   public Object evaluateColor(float fraction, Object startValue, Object endValue) {
       int startInt = (Integer) startValue;
       int startA = (startInt >> 24) & 0xff;
       int startR = (startInt >> 16) & 0xff;
       int startG = (startInt >> 8) & 0xff;
       int startB = startInt & 0xff;

       int endInt = (Integer) endValue;
       int endA = (endInt >> 24) & 0xff;
       int endR = (endInt >> 16) & 0xff;
       int endG = (endInt >> 8) & 0xff;
       int endB = endInt & 0xff;

       return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
               (int)((startR + (int)(fraction * (endR - startR))) << 16) |
               (int)((startG + (int)(fraction * (endG - startG))) << 8) |
               (int)((startB + (int)(fraction * (endB - startB))));
   }

//------------侧滑菜单事件监听------以下------------


   /**
    * 事件监听(打开,关闭,拖动)
    */
   private void listenDragState() {
       int left = mMainView.getLeft();
       if(left == 0){
           mCurrentStatus = DragStatus.CLOSE;//关闭
       }else if(left == mMRange){
           mCurrentStatus = DragStatus.OPEN;//打开
       }else{
           mCurrentStatus = DragStatus.DRAGGING;//拖动
       }

       //当事件发生时,回调监听器中的方法
       if(mOnDragListener != null){
           if(mCurrentStatus == DragStatus.OPEN){

               //回调监听器中相应的方法

           }else if(mCurrentStatus == DragStatus.CLOSE){

               mOnDragListener.onClose();

           }else{

               float v = ((float) mMainView.getLeft()) / mMRange;
               mOnDragListener.onDraging(v);
           }
       }

   }

   public boolean isOpen() {

       return  mCurrentStatus == DragStatus.OPEN;
   }


   //定义一个枚举
   public enum DragStatus{
       OPEN,CLOSE,DRAGGING
   }


   private DragStatus mCurrentStatus = DragStatus.CLOSE;//默认状态

   /**
    * 提供状态获取
    * @return
    */
   public DragStatus getCurrentStatus() {
       return mCurrentStatus;
   }

   /**
    * 1.自定义的监听器
    */
   private OnDragListener mOnDragListener;
   public interface OnDragListener {
       void onOpen();
       void onClose();
       void onDraging(float value);

   }
   /**
    * 给外部提供设置监听器的方法
    * @param onDragListener
    */
   public void setOnDragListener(OnDragListener onDragListener){
       mOnDragListener = onDragListener;
   }

   //--------------事件监听-----以上---------------------------

   private int fixDragLeft(int left) {
       if (left < 0) {
           left = 0;
       } else if (left > mMRange) {
           left = mMRange;
       }
       return left;
   }

   /**
    * 关闭菜单
    */
   private void close() {
       mMenuView.layout(0, 0, mWidth, mHeight);
       //  mMainView.layout(0,0,mWidth,mHeight);
       //主界面平滑滚动
       mDragHelper.smoothSlideViewTo(mMainView, 0, 0);
       //刷新 调用顺序Invalidate --> onDraw() --> computeScroll(重写,在此方法里面需要继续刷新)
       ViewCompat.postInvalidateOnAnimation(this);
   }

   /**
    * 打开菜单
    */
   private void open() {
       mMenuView.layout(0, 0, mWidth, mHeight);
       //mMainView.layout(mMRange,0,mWidth+mMRange,mHeight);
       // 平滑滚动到某一位置: 第一步
       mDragHelper.smoothSlideViewTo(mMainView, mMRange, 0);

       //刷新 调用顺序Invalidate --> onDraw() --> computeScroll(重写,在此方法里面需要继续刷新)
       ViewCompat.postInvalidateOnAnimation(this);
   }

   @Override
   public void computeScroll() {
       super.computeScroll();

       //第二步
       //如果没有滑动到指定位置,需要继续刷新
       if (mDragHelper.continueSettling(true)) {
           ViewCompat.postInvalidateOnAnimation(this);
       }
   }

   private void init() {
       //1.创建ViewDragHelper对象
       float sensitivity = 1.0f;//敏感值,数值越大则越容易滑动菜单
       mDragHelper = ViewDragHelper.create(this, sensitivity, mCallack);
   }

   @Override
   public boolean onInterceptTouchEvent(MotionEvent ev) {

       // 2. 让ViewDragHelper决定是否拦截事件
       return mDragHelper.shouldInterceptTouchEvent(ev);
   }

   @Override
   public boolean onTouchEvent(MotionEvent event) {
       // 3. 把事件交给ViewDragHelper处理
       mDragHelper.processTouchEvent(event);
       //按下时需要返回true,否则无法接收到后续的move和up事件
       if (event.getAction() == MotionEvent.ACTION_DOWN) {
           return true;
       }

       return super.onTouchEvent(event);
   }

   /**
    * 当填充结束后回调此方法,注意:不能在此方法获取控件的宽高
    */
   @Override
   protected void onFinishInflate() {
       super.onFinishInflate();
       // 健壮性处理
       if (getChildCount() < 2) {
           throw new IllegalStateException("DragLayout至少要有两个子控件");
       }
       mMenuView = getChildAt(0);
       mMainView = getChildAt(1);
   }

   /**
    * 执行此方法时,已经调用完了onMeasure,所以可以获取控件宽高
    *
    * @param w
    * @param h
    * @param oldw
    * @param oldh
    */
   @Override
   protected void onSizeChanged(int w, int h, int oldw, int oldh) {
       super.onSizeChanged(w, h, oldw, oldh);
       //获取控件的宽高
       mWidth = getMeasuredWidth();
       mHeight = getMeasuredHeight();
       //设置主界面只能滑到 %60;
       mMRange = (int) (mWidth * 0.6f);
   }
}

头像左右晃动的实现

监听当侧滑面板关闭时通过属性动画实现左右晃动的动画。

mDragLayout.setOnDragListener(new OnDragListener() {    
    @Override
    public void open() {
    }   
    @Override
    public void onDragging(float percent) {
        iv_header.setAlpha(1 - percent); // 拖动时设置透明度
    }   
    @Override
    public void close() {
        showToast("close");     


    TranslateAnimation animation = new TranslateAnimation(0, 10, 0, 0);
    animation.setDuration(100);
    animation.setRepeatCount(4);
    iv_header.startAnimation(animation);


    }
});

问题解决

处理菜单打开后主界面列表仍可滑动的问题

自定义主界面根布局控件,并重写拦截方法,当侧滑菜单打开时返回true拦截事件:

    public class MyLinearLayout extends LinearLayout {

        private DragLayout dragLayout;

        public MyLinearLayout(Context context) {
            super(context);
        }
        public MyLinearLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public void setDragLayout(DragLayout dragLayout) {
            this.dragLayout = dragLayout;
        }   
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            // 如果侧菜单打开,则拦截事件,不让列表点击或者滚动
            if (dragLayout.isOpen()) {
                return true;
            }
            return super.onInterceptTouchEvent(ev);
        }
         @Override
        public boolean onTouchEvent(MotionEvent event) {
            //如果菜单打开,不允许触摸主界面时,菜单列表滑动
            if(layout!= null && layout.isOpen()){
                return true;
            }
            return super.onTouchEvent(event);
        }
    }

完整源码:点击下载

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章