DragLayout: QQ5.0侧拉菜单的新特效

Posted 吴小

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DragLayout: QQ5.0侧拉菜单的新特效相关的知识,希望对你有一定的参考价值。

一、项目概要

  1.1 项目效果如图:

  gif

  

  1.2 需要使用到的技术

     ViewDragHelper: 要实现和QQ5.0侧滑的特效,需要借助谷歌在2013I/O大会上发布的ViewDragHelper类,提供这个类目的就是为了解决拖拽滑动问题

 

  1.3 侧滑菜单的实现方式

    1. SlidingMenu 第三方库

    2. DrawerLayout v4包中的类

    3. 自定义控件 

 

  1.4 一些回调方法 

    - tryCaptureView: 用来决定是否可以拖动
    - clampViewPositionHorizontal: 用来设置子控件将要显示的位置 [限制子控件拖动的范围]
    - getViewHorizontalDragRange:返回水平方向拖动的最大范围,返回大于0的值才可以拖动
    - onViewPositionChanged: 位置改变时调用 [关联菜单与主界面的滑动,监听拖动状态,伴随动画]
    - onViewReleased: 拖动结束后,松开手时调用 [平滑地打开或关闭侧滑菜单]

 

二、项目实现

  2.1 创建DragLayout

1 public class DragLayout extends FrameLayout {
2     public DragLayout(Context context) {
3         super(context);
4     }
5     public DragLayout(Context context, AttributeSet attrs) {
6         super(context, attrs);
7     }
8 }

 

    2.2 创建侧滑面板布局

 1 <com.xiaowu.draglayout.view.DragLayout 
 2     xmlns:android="http://schemas.android.com/apk/res/android"
 3     xmlns:tools="http://schemas.android.com/tools"
 4     android:id="@+id/drag_layout"
 5     android:layout_width="match_parent"
 6     android:layout_height="match_parent"
 7     android:background="@drawable/bg" >
 8 
 9     <!-- 侧滑菜单布局 -->
10     <LinearLayout
11         android:layout_width="match_parent"
12         android:layout_height="match_parent"
13         android:background="#33ff0000" />
14 
15     <!-- 主界面布局 -->
16     <LinearLayout
17         android:layout_width="match_parent"
18         android:layout_height="match_parent"
19         android:background="#3300ff00" />
20 
21 </com.xiaowu.draglayout.view.DragLayout>

 

      2.3 DragLayout的主程序代码,下面代码中有详细的讲解,我就不多分步骤实现了

  1 package com.xiaowu.draglayout.view;
  2 
  3 import android.content.Context;
  4 import android.graphics.Color;
  5 import android.graphics.PorterDuff;
  6 import android.graphics.drawable.Drawable;
  7 import android.support.v4.view.ViewCompat;
  8 import android.support.v4.widget.ViewDragHelper;
  9 import android.util.AttributeSet;
 10 import android.view.MotionEvent;
 11 import android.view.View;
 12 import android.widget.FrameLayout;
 13 
 14 /**
 15  * Created by ${VINCENT} on 2016/11/8.
 16  */
 17 
 18 public class DragLayout extends FrameLayout {
 19 
 20     private ViewDragHelper mViewDragHelper;
 21     private View mMenuView;
 22     private View mMainView;
 23     private int mRange;
 24     private int mWidth;
 25     private int mHeight;
 26 
 27     public DragLayout(Context context) {
 28         super(context);
 29         init();
 30     }
 31 
 32     public DragLayout(Context context, AttributeSet attrs) {
 33         super(context, attrs);
 34         init();
 35     }
 36 
 37     /** 填充完成后调用此方法 */
 38     @Override
 39     protected void onFinishInflate() {
 40         super.onFinishInflate();
 41         // 健壮性判断
 42         if (getChildCount() < 2) {
 43             throw new IllegalStateException("DrawLayout至少要有两个子控件");
 44         }
 45         mMenuView = getChildAt(0);
 46         mMainView = getChildAt(1);
 47     }
 48 
 49     // step1:创建ViewDragHelper对象
 50     private void init() {
 51         float sensitivity = 1.0f; //值越大,灵敏度越高
 52         mViewDragHelper = ViewDragHelper.create(this, sensitivity, mCallBack);
 53     }
 54 
 55     // step2:由ViewDragHelper决定是否拦截事件
 56     @Override
 57     public boolean onInterceptTouchEvent(MotionEvent ev) {
 58         return mViewDragHelper.shouldInterceptTouchEvent(ev);
 59     }
 60 
 61     // step3:把触摸事件交给ViewDragHelper处理
 62     @Override
 63     public boolean onTouchEvent(MotionEvent event) {
 64         mViewDragHelper.processTouchEvent(event);
 65         return true; //让mViewDragHelper持续接收到触摸事件
 66     }
 67 
 68     // step4:处理ViewDragHelper的Callback方法
 69     ViewDragHelper.Callback mCallBack = new ViewDragHelper.Callback() {
 70 
 71         // (1)捕获子控件,返回true表示子控件可以拖动
 72         @Override
 73         public boolean tryCaptureView(View child, int pointerId) {
 74             return true;
 75         }
 76 
 77         // (2)子控件显示的方向(horizontal, vertical)
 78         // left: 被拖动控件的将要显示的位置
 79         // dx: 位置的偏移量 = left - 当前的left
 80         @Override
 81         public int clampViewPositionHorizontal(View child, int left, int dx) {
 82             if (child == mMainView) {
 83                 left = reviseLeft(left);
 84             }
 85             return left;
 86         }
 87 
 88         // (3)返回水平方向拖动的最大范围mRange,内部会根据返回值计算动画执行的时间
 89         @Override
 90         public int getViewHorizontalDragRange(View child) {
 91             return mRange;
 92         }
 93 
 94         // (4)位置发生改变的回调
 95         @Override
 96         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
 97             // (a) 关联子控件的滑动
 98             if (changedView == mMenuView) {
 99                 // 侧拉菜单界面不变时
100                 mMenuView.layout(0, 0, mWidth, mHeight);
101                 // 主菜单界面的新位置
102                 int newLeft = mMenuView.getLeft() + dx;
103                 newLeft = reviseLeft(newLeft);
104                 mMainView.layout(newLeft, 0, mWidth + newLeft, mHeight);
105             }
106             // (b) 事件的监听(打开,拖动,关闭)
107             listenDragStatus();
108             // (c) 事件伴随的动画
109             animateChildren();
110         }
111 
112         // (5) 拖动结束时回调的方法
113         // xvel:释放时的回调速度,在这里向右为正
114         @Override
115         public void onViewReleased(View releasedChild, float xvel, float yvel) {
116             if (xvel > 0) {
117                 open();
118             } else if (xvel == 0 && mMainView.getLeft() > mRange / 2) {
119                 open();
120             } else {
121                 close();
122             }
123         }
124     };
125 
126     //============================动画的定义=====================================
127     /** 估值器:变化值 = 开始值 + (结束值 - 开始值) * 百分比 */
128     public float evaluate(float start, float end, float percent) {
129         return start + (end - start) * percent;
130     }
131 
132     protected void animateChildren() {
133         float percent = ((float) mMainView.getLeft()) / mRange;
134 
135         // 1.主界面的缩放
136         mMainView.setScaleX(evaluate(1f, 0.8f, percent));
137         mMainView.setScaleY(evaluate(1f, 0.8f, percent));
138         // 2.侧拉菜单的缩放
139         mMenuView.setTranslationX((int) evaluate(-mRange, 0, percent)); // 平移
140         mMenuView.setScaleX(evaluate(0.5f, 1.0f, percent));
141         mMenuView.setScaleY(evaluate(0.5f, 1.0f, percent));
142         mMenuView.setAlpha(evaluate(0.5f, 1.0f, percent));
143         // 3.背景图片:亮度的变化
144         Drawable background = getBackground();
145         if (background != null) {
146             // 过渡的颜色
147             int color = (int)evaluate2(percent, Color.BLACK, Color.TRANSPARENT);
148             background.setColorFilter(color, PorterDuff.Mode.SRC_OVER);
149         }
150     }
151 
152     /** 处理颜色渐变的兼容性问题 */
153     public Object evaluate2(float fraction, Object startValue, Object endValue) {
154         int startInt = (Integer) startValue;
155         int startA = (startInt >> 24) & 0xff;
156         int startR = (startInt >> 16) & 0xff;
157         int startG = (startInt >> 8) & 0xff;
158         int startB = startInt & 0xff;
159 
160         int endInt = (Integer) endValue;
161         int endA = (endInt >> 24) & 0xff;
162         int endR = (endInt >> 16) & 0xff;
163         int endG = (endInt >> 8) & 0xff;
164         int endB = endInt & 0xff;
165 
166         return ((startA + (int)(fraction * (endA - startA))) << 24) |
167                 ((startR + (int)(fraction * (endR - startR))) << 16) |
168                 ((startG + (int)(fraction * (endG - startG))) << 8) |
169                 ((startB + (int)(fraction * (endB - startB))));
170     }
171 
172     //============================状态的监听begin================================
173     /** 事件的监听 */
174     protected void listenDragStatus() {
175         int left = mMainView.getLeft();
176         if (left == 0) {
177             mCurrentStatus = DragStatus.CLOSE;
178         } else if (left == mRange) {
179             mCurrentStatus = DragStatus.OPEN;
180         } else {
181             mCurrentStatus = DragStatus.DRAGGING;
182         }
183 
184         //当事件发生时,调用监听器中的方法
185         if (mOnDragListener != null) {
186             if (mCurrentStatus == DragStatus.OPEN) {
187                 mOnDragListener.onOpen();
188             } else if (mCurrentStatus == DragStatus.CLOSE) {
189                 mOnDragListener.onClose();
190             } else {
191                 float percent = ((float) mMainView.getLeft()) / mRange;
192                 mOnDragListener.onDragging(percent);
193             }
194         }
195     }
196 
197     /** 状态的定义 */
198     public enum DragStatus {
199         OPEN, CLOSE, DRAGGING
200     }
201 
202     /** 当前的状态 */
203     private DragStatus mCurrentStatus = DragStatus.CLOSE;
204 
205     public DragStatus getCurrentStatus() {
206         return mCurrentStatus;
207     }
208 
209     /** 定义接口 */
210     public interface OnDragListener {
211         void onOpen();
212         void onClose();
213         void onDragging(float percent);
214     }
215 
216     private OnDragListener mOnDragListener;
217 
218     /** 提供设置监听器的set方法 */
219     public void setOnDragListener(OnDragListener onDragListener) {
220         this.mOnDragListener = onDragListener;
221     }
222 
223     //============================状态的监听end================================
224 
225     @Override
226     public void computeScroll() {
227         super.computeScroll();
228         // 若如果没有移动到正确的位置,需要刷新
229         if (mViewDragHelper.continueSettling(true)) {
230             ViewCompat.postInvalidateOnAnimation(this);
231         }
232     }
233 
234     /** 限定主界面的滑动范围 */
235     protected int reviseLeft(int left) {
236         if (left < 0) {
237             left = 0;
238         } else if (left > mRange) {
239             left = mRange;
240         }
241         return left;
242     }
243 
244     /** 控件尺寸发生改变时,回调该方法 */
245     @Override
246     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
247         super.onSizeChanged(w, h, oldw, oldh);
248         // 获取DrawLayout的宽高
249         mWidth = getMeasuredWidth();
250         mHeight = getMeasuredHeight();
251         // 拖拽的比例
252         mRange = (int) (mWidth * 0.6f);
253     }
254 
255     /** 打开侧拉菜单 */
256     protected void open() {
257         mViewDragHelper.smoothSlideViewTo(mMainView, mRange, 0);
258         // 刷新界面
259         ViewCompat.postInvalidateOnAnimation(this);
260     }
261 
262     /** 关闭侧拉菜单 */
263     protected void close() {
264         mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
265         // 刷新界面
266         ViewCompat.postInvalidateOnAnimation(this);
267     }
268 
269     /** 侧滑菜单是否打开 */
270     public boolean isOpen() {
271         return mCurrentStatus == DragStatus.OPEN;
272     }
273 
274 }

  

   2.4 创建MyLinearLayout.java文件,处理侧拉与主菜单的冲突事件

 1 package com.xiaowu.draglayout.view;
 2 
 3 import android.content.Context;
 4 import android.util.AttributeSet;
 5 import android.view.MotionEvent;
 6 import android.widget.LinearLayout;
 7 
 8 /**
 9  * Created by ${VINCENT} on 2016/11/9.
10  */
11 
12 public class MyLinearLayout extends LinearLayout {
13 
14     private DragLayout mDragLayout;
15 
16     public MyLinearLayout(Context context) {
17         super(context);
18     }
19 
20     public MyLinearLayout(Context context, AttributeSet attrs) {
21         super(context, attrs);
22     }
23 
24     /** 根据它的打开状态决定是否要拦截事件 */
25     public void setDragLayout(DragLayout dragLayout) {
26         this.mDragLayout = dragLayout;
27     }
28 
29     /** 如果侧滑菜单打开了,禁止主菜单的列表滑动 */
30     @Override
31     public boolean onInterceptTouchEvent(MotionEvent ev) {
32         if (mDragLayout.isOpen()) {
33             return true;
34         }
35         return super.onInterceptTouchEvent(ev);
36     }
37 
38     /** 如果侧滑菜单打开了,消费主菜单的触摸事件,禁止通过滑动主菜单使侧拉菜单的列表滑动 */
39     @Override
40     public boolean onTouchEvent(MotionEvent event) {
41         if (mDragLayout.isOpen()) {
42             return true;
43         }
44         return super.onTouchEvent(event);
45     }
46 }

 

   2.5 接下来是MainActivity的代码实现

 1 package com.xiaowu.draglayout;
 2 
 3 import android.graphics.Color;
 4 import android.support.v7.app.AppCompatActivity;
 5 import android.os.Bundle;
 6 import android.view.View;
 7 import android.view.ViewGroup;
 8 import android.view.Window;
 9 import android.widget.ArrayAdapter;
10 import android.widget.ImageView;
11 import android.widget.ListView;
12 import android.widget.TextView;
13 import android.widget.Toast;
14 
15 import com.xiaowu.draglayout.view.DragLayout;
16 import com.xiaowu.draglayout.view.MyLinearLayout;
17 
18 public class MainActivity extends AppCompatActivity {
19 
20     private ImageView mIvHeader;
21     private MyLinearLayout mMyLinearLayout;
22     private DragLayout mDragLayout;
23 
24     @Override
25     protected void onCreate(Bundle savedInstanceState) {
26         super.onCreate(savedInstanceState);
27         requestWindowFeature(Window.FEATURE_NO_TITLE);
28 
29         setContentView(R.layout.activity_main);
30         mIvHeader = (ImageView) findViewById(R.id.iv_header);
31 
32         initDragLayout();
33         mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_ll);
34         // 根据打开的状态决定是否拦截事件
35         mMyLinearLayout.setDragLayout(mDragLayout);
36 
37         initListView();
38     }
39 
以上是关于DragLayout: QQ5.0侧拉菜单的新特效的主要内容,如果未能解决你的问题,请参考以下文章

Android 左滑or右滑抽屉菜单

Drawerlayout侧拉菜单

仿qq的侧拉菜单效果

自定义控件基础02_下拉刷新_侧拉菜单_自定义属性

事件拦截,仿qq侧拉的操作中

自定义View ----QQ5.0左边侧滑 + 动画