你的位置:首页 > 操作系统

[操作系统]DragLayout: QQ5.0侧拉菜单的新特效


一、项目概要

  1.1 项目效果如图:

  gif

  

  1.2 需要使用到的技术

     ViewDragHelper: 要实现和QQ5.0侧滑的特效,需要借助谷歌在2013年I/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   ="http://schemas.android.com/apk/res/android" 3   ="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   <LinearLayout11     android:layout_width="match_parent"12     android:layout_height="match_parent"13     android:background="#33ff0000" />14 15   <!-- 主界面布局 -->16   <LinearLayout17     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     @Override115     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, DRAGGING200   }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   @Override226   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   @Override246   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   @Override31   public boolean onInterceptTouchEvent(MotionEvent ev) {32     if (mDragLayout.isOpen()) {33       return true;34     }35     return super.onInterceptTouchEvent(ev);36   }37 38   /** 如果侧滑菜单打开了,消费主菜单的触摸事件,禁止通过滑动主菜单使侧拉菜单的列表滑动 */39   @Override40   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   @Override25   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 40   private void initListView() {41     ListView lvMenu = (ListView) findViewById(R.id.lv_menu);42     ListView lvMain = (ListView) findViewById(R.id.lv_main);43 44     lvMenu.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,45         Constant.MENUS){46       @Override47       public View getView(int position, View convertView, ViewGroup parent) {48         TextView view = (TextView) super.getView(position, convertView, parent);49         view.setTextSize(dp2px(16));50         view.setTextColor(Color.WHITE);51         return view;52       }53     });54 55     lvMain.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,56         Constant.LIST_DATAS));57 58   }59 60   private void initDragLayout() {61     mDragLayout = (DragLayout) findViewById(R.id.drag_layout);62     mDragLayout.setOnDragListener(new DragLayout.OnDragListener() {63       @Override64       public void onOpen() {65         showToast("打开");66       }67 68       @Override69       public void onClose() {70         showToast("关闭");71       }72 73       @Override74       public void onDragging(float percent) {75         mIvHeader.setAlpha(1 - percent );76       }77     });78   }79 80   /** toast使用单例模式,可以随状态刷新 */81   private Toast mToast;82 83   public void showToast(String msg) {84     if (mToast == null) {85       mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);86     }87     mToast.setText(msg);88     mToast.show();89   }90 91   public int dp2px(int dp) {92     float density = this.getResources().getDisplayMetrics().density;93     return (int) (dp * density + 0.5f);94   }95 96 }

 

   2.6 布局文件的最终完善

 1 <com.xiaowu.draglayout.view.DragLayout 2   ="http://schemas.android.com/apk/res/android" 3   android:id="@+id/drag_layout" 4   android:layout_width="match_parent" 5   android:layout_height="match_parent" 6   android:background="@drawable/bg"> 7  8   <!-- 侧拉菜单界面 --> 9   <LinearLayout10     android:layout_width="match_parent"11     android:layout_height="match_parent"12     android:orientation="vertical"13     android:gravity="center_vertical"14     android:padding="15dp" >15 16     <ImageView17       android:layout_width="40dp"18       android:layout_height="40dp"19       android:background="@drawable/head"/>20 21     <ListView22       android:id="@+id/lv_menu"23       android:layout_width="match_parent"24       android:layout_height="match_parent"25       android:layout_marginTop="10dp" />26 27   </LinearLayout>28 29   <!-- 主菜单界面 -->30   <com.xiaowu.draglayout.view.MyLinearLayout31     android:id="@+id/my_ll"32     android:layout_width="match_parent"33     android:layout_height="match_parent"34     android:orientation="vertical"35     android:gravity="center"36     android:background="#fff">37     <RelativeLayout38       android:layout_width="match_parent"39       android:layout_height="45dp"40       android:background="#18B4ED" >41 42       <ImageView43         android:id="@+id/iv_header"44         android:layout_width="40dp"45         android:layout_height="40dp"46         android:layout_centerVertical="true"47         android:layout_marginLeft="10dp"48         android:background="@drawable/head" />49     </RelativeLayout>50 51     <ListView52       android:id="@+id/lv_main"53       android:layout_width="match_parent"54       android:layout_height="match_parent"55       android:layout_weight="1" />56 57   </com.xiaowu.draglayout.view.MyLinearLayout>58 59 </com.xiaowu.draglayout.view.DragLayout>

 

   2.7 Constant静态类可以自己定义,不过我还是善良的贴了出来

package com.xiaowu.draglayout;public class Constant {  /** 菜单列表数据 */  public static final String[] MENUS = new String[] {    "纸杯蛋糕[Cupcake]",    "甜甜圈[Donut]",    "闪电泡芙[Eclair]",    "冻酸奶[Froyo]",    "姜饼[Gingerbread]",    "蜂巢[Honeycomb]",    "冰淇淋三明治[Ice Cream Sandwich]",    "果冻豆[Jelly Bean]",    "奇巧[KitKat]",    "棒棒糖[Lollipop]",    "姜饼[Gingerbread]",    "蜂巢[Honeycomb]",    "冰淇淋三明治[Ice Cream Sandwich]",    "果冻豆[Jelly Bean]",    "奇巧[KitKat]",    "棒棒糖[Lollipop]",    "棉花糖[Marshmallow]"  };    /** 列表数据1 */  public static final String[] LIST_DATAS = {    "API1--1.0 [没有开发代号]",    "API2--1.1 Petit Four",    "API3--1.5 Cupcake",    "API4--1.6 Donut",    "API5--2.0 Eclair",    "API6--2.0.1 Eclair",    "API7--2.1 Eclair",    "API8--2.2 - 2.2.3 Froyo",    "API9--2.3 - 2.3.2 Gingerbread",    "API10--2.3.3-2.3.7 Gingerbread",    "API11--3.0 Honeycomb",    "API12--3.1 Honeycomb",    "API13--3.2 Honeycomb",    "API14--4.0 - 4.0.2 Ice Cream Sandwich",    "API15--4.0.3 - 4.0.4 Ice Cream Sandwich",    "API16--4.1 Jelly Bean",    "API17--4.2 Jelly Bean",    "API18--4.3 Jelly Bean",    "API19--4.4 KitKat",    "API20--4.4W",    "API21--5.0 Lollipop",    "API22--5.1 Lollipop",    "API23--6.0 Marshmallow"  };}

 

三、一些可以借鉴的东西

  *比如使用Toast的时候可以采用单例模式,使得Toast可以随时改变,而不会产生停顿延迟的问题(顶部效果图)

 1 /** toast使用单例模式,可以随状态刷新 */ 2   private Toast mToast; 3  4   public void showToast(String msg) { 5     if (mToast == null) { 6       mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG); 7     } 8     mToast.setText(msg); 9     mToast.show();10   }

 

四、提供给博友我的源代码

    **下载链接:http://pan.baidu.com/s/1o8k4cZo  密码:m0fl