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);
        }
    }

完整源碼:點擊下載

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