一、项目概要
1.1 项目效果如图:
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
public class DragLayout extends FrameLayout { public DragLayout(Context context) { super(context); } public DragLayout(Context context, AttributeSet attrs) { super(context, attrs); } }
2.2 创建侧滑面板布局
<com.xiaowu.draglayout.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drag_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg" > <!-- 侧滑菜单布局 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#33ff0000" /> <!-- 主界面布局 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#3300ff00" /> </com.xiaowu.draglayout.view.DragLayout>
2.3 DragLayout的主程序代码,下面代码中有详细的讲解,我就不多分步骤实现了
package com.xiaowu.draglayout.view; import android.content.Context; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; /** * Created by ${VINCENT} on 2016/11/8. */ public class DragLayout extends FrameLayout { private ViewDragHelper mViewDragHelper; private View mMenuView; private View mMainView; private int mRange; private int mWidth; private int mHeight; public DragLayout(Context context) { super(context); init(); } public DragLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } /** 填充完成后调用此方法 */ @Override protected void onFinishInflate() { super.onFinishInflate(); // 健壮性判断 if (getChildCount() < 2) { throw new IllegalStateException("DrawLayout至少要有两个子控件"); } mMenuView = getChildAt(0); mMainView = getChildAt(1); } // step1:创建ViewDragHelper对象 private void init() { float sensitivity = 1.0f; //值越大,灵敏度越高 mViewDragHelper = ViewDragHelper.create(this, sensitivity, mCallBack); } // step2:由ViewDragHelper决定是否拦截事件 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mViewDragHelper.shouldInterceptTouchEvent(ev); } // step3:把触摸事件交给ViewDragHelper处理 @Override public boolean onTouchEvent(MotionEvent event) { mViewDragHelper.processTouchEvent(event); return true; //让mViewDragHelper持续接收到触摸事件 } // step4:处理ViewDragHelper的Callback方法 ViewDragHelper.Callback mCallBack = new ViewDragHelper.Callback() { // (1)捕获子控件,返回true表示子控件可以拖动 @Override public boolean tryCaptureView(View child, int pointerId) { return true; } // (2)子控件显示的方向(horizontal, vertical) // left: 被拖动控件的将要显示的位置 // dx: 位置的偏移量 = left - 当前的left @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (child == mMainView) { left = reviseLeft(left); } return left; } // (3)返回水平方向拖动的最大范围mRange,内部会根据返回值计算动画执行的时间 @Override public int getViewHorizontalDragRange(View child) { return mRange; } // (4)位置发生改变的回调 @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { // (a) 关联子控件的滑动 if (changedView == mMenuView) { // 侧拉菜单界面不变时 mMenuView.layout(0, 0, mWidth, mHeight); // 主菜单界面的新位置 int newLeft = mMenuView.getLeft() + dx; newLeft = reviseLeft(newLeft); mMainView.layout(newLeft, 0, mWidth + newLeft, mHeight); } // (b) 事件的监听(打开,拖动,关闭) listenDragStatus(); // (c) 事件伴随的动画 animateChildren(); } // (5) 拖动结束时回调的方法 // xvel:释放时的回调速度,在这里向右为正 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if (xvel > 0) { open(); } else if (xvel == 0 && mMainView.getLeft() > mRange / 2) { open(); } else { close(); } } }; //============================动画的定义===================================== /** 估值器:变化值 = 开始值 + (结束值 - 开始值) * 百分比 */ public float evaluate(float start, float end, float percent) { return start + (end - start) * percent; } protected void animateChildren() { float percent = ((float) mMainView.getLeft()) / mRange; // 1.主界面的缩放 mMainView.setScaleX(evaluate(1f, 0.8f, percent)); mMainView.setScaleY(evaluate(1f, 0.8f, percent)); // 2.侧拉菜单的缩放 mMenuView.setTranslationX((int) evaluate(-mRange, 0, percent)); // 平移 mMenuView.setScaleX(evaluate(0.5f, 1.0f, percent)); mMenuView.setScaleY(evaluate(0.5f, 1.0f, percent)); mMenuView.setAlpha(evaluate(0.5f, 1.0f, percent)); // 3.背景图片:亮度的变化 Drawable background = getBackground(); if (background != null) { // 过渡的颜色 int color = (int)evaluate2(percent, Color.BLACK, Color.TRANSPARENT); background.setColorFilter(color, PorterDuff.Mode.SRC_OVER); } } /** 处理颜色渐变的兼容性问题 */ public Object evaluate2(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 ((startA + (int)(fraction * (endA - startA))) << 24) | ((startR + (int)(fraction * (endR - startR))) << 16) | ((startG + (int)(fraction * (endG - startG))) << 8) | ((startB + (int)(fraction * (endB - startB)))); } //============================状态的监听begin================================ /** 事件的监听 */ protected void listenDragStatus() { int left = mMainView.getLeft(); if (left == 0) { mCurrentStatus = DragStatus.CLOSE; } else if (left == mRange) { mCurrentStatus = DragStatus.OPEN; } else { mCurrentStatus = DragStatus.DRAGGING; } //当事件发生时,调用监听器中的方法 if (mOnDragListener != null) { if (mCurrentStatus == DragStatus.OPEN) { mOnDragListener.onOpen(); } else if (mCurrentStatus == DragStatus.CLOSE) { mOnDragListener.onClose(); } else { float percent = ((float) mMainView.getLeft()) / mRange; mOnDragListener.onDragging(percent); } } } /** 状态的定义 */ public enum DragStatus { OPEN, CLOSE, DRAGGING } /** 当前的状态 */ private DragStatus mCurrentStatus = DragStatus.CLOSE; public DragStatus getCurrentStatus() { return mCurrentStatus; } /** 定义接口 */ public interface OnDragListener { void onOpen(); void onClose(); void onDragging(float percent); } private OnDragListener mOnDragListener; /** 提供设置监听器的set方法 */ public void setOnDragListener(OnDragListener onDragListener) { this.mOnDragListener = onDragListener; } //============================状态的监听end================================ @Override public void computeScroll() { super.computeScroll(); // 若如果没有移动到正确的位置,需要刷新 if (mViewDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } /** 限定主界面的滑动范围 */ protected int reviseLeft(int left) { if (left < 0) { left = 0; } else if (left > mRange) { left = mRange; } return left; } /** 控件尺寸发生改变时,回调该方法 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 获取DrawLayout的宽高 mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); // 拖拽的比例 mRange = (int) (mWidth * 0.6f); } /** 打开侧拉菜单 */ protected void open() { mViewDragHelper.smoothSlideViewTo(mMainView, mRange, 0); // 刷新界面 ViewCompat.postInvalidateOnAnimation(this); } /** 关闭侧拉菜单 */ protected void close() { mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0); // 刷新界面 ViewCompat.postInvalidateOnAnimation(this); } /** 侧滑菜单是否打开 */ public boolean isOpen() { return mCurrentStatus == DragStatus.OPEN; } }
2.4 创建MyLinearLayout.java文件,处理侧拉与主菜单的冲突事件
package com.xiaowu.draglayout.view; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.LinearLayout; /** * Created by ${VINCENT} on 2016/11/9. */ public class MyLinearLayout extends LinearLayout { private DragLayout mDragLayout; public MyLinearLayout(Context context) { super(context); } public MyLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } /** 根据它的打开状态决定是否要拦截事件 */ public void setDragLayout(DragLayout dragLayout) { this.mDragLayout = dragLayout; } /** 如果侧滑菜单打开了,禁止主菜单的列表滑动 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mDragLayout.isOpen()) { return true; } return super.onInterceptTouchEvent(ev); } /** 如果侧滑菜单打开了,消费主菜单的触摸事件,禁止通过滑动主菜单使侧拉菜单的列表滑动 */ @Override public boolean onTouchEvent(MotionEvent event) { if (mDragLayout.isOpen()) { return true; } return super.onTouchEvent(event); } }
2.5 接下来是MainActivity的代码实现
package com.xiaowu.draglayout; import android.graphics.Color; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.xiaowu.draglayout.view.DragLayout; import com.xiaowu.draglayout.view.MyLinearLayout; public class MainActivity extends AppCompatActivity { private ImageView mIvHeader; private MyLinearLayout mMyLinearLayout; private DragLayout mDragLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); mIvHeader = (ImageView) findViewById(R.id.iv_header); initDragLayout(); mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_ll); // 根据打开的状态决定是否拦截事件 mMyLinearLayout.setDragLayout(mDragLayout); initListView(); } private void initListView() { ListView lvMenu = (ListView) findViewById(R.id.lv_menu); ListView lvMain = (ListView) findViewById(R.id.lv_main); lvMenu.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, Constant.MENUS){ @Override public View getView(int position, View convertView, ViewGroup parent) { TextView view = (TextView) super.getView(position, convertView, parent); view.setTextSize(dp2px(16)); view.setTextColor(Color.WHITE); return view; } }); lvMain.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, Constant.LIST_DATAS)); } private void initDragLayout() { mDragLayout = (DragLayout) findViewById(R.id.drag_layout); mDragLayout.setOnDragListener(new DragLayout.OnDragListener() { @Override public void onOpen() { showToast("打开"); } @Override public void onClose() { showToast("关闭"); } @Override public void onDragging(float percent) { mIvHeader.setAlpha(1 - percent ); } }); } /** toast使用单例模式,可以随状态刷新 */ private Toast mToast; public void showToast(String msg) { if (mToast == null) { mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG); } mToast.setText(msg); mToast.show(); } public int dp2px(int dp) { float density = this.getResources().getDisplayMetrics().density; return (int) (dp * density + 0.5f); } }
2.6 布局文件的最终完善
<com.xiaowu.draglayout.view.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/drag_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg"> <!-- 侧拉菜单界面 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center_vertical" android:padding="15dp" > <ImageView android:layout_width="40dp" android:layout_height="40dp" android:background="@drawable/head"/> <ListView android:id="@+id/lv_menu" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="10dp" /> </LinearLayout> <!-- 主菜单界面 --> <com.xiaowu.draglayout.view.MyLinearLayout android:id="@+id/my_ll" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:background="#fff"> <RelativeLayout android:layout_width="match_parent" android:layout_height="45dp" android:background="#18B4ED" > <ImageView android:id="@+id/iv_header" android:layout_width="40dp" android:layout_height="40dp" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:background="@drawable/head" /> </RelativeLayout> <ListView android:id="@+id/lv_main" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" /> </com.xiaowu.draglayout.view.MyLinearLayout> </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可以随时改变,而不会产生停顿延迟的问题(顶部效果图)
/** toast使用单例模式,可以随状态刷新 */ private Toast mToast; public void showToast(String msg) { if (mToast == null) { mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG); } mToast.setText(msg); mToast.show(); }
四、提供给博友我的源代码
**下载链接:
http://pan.baidu.com/s/1o8k4cZo 密码:m0fl