Android使用ViewDragHelper实现QQ6.X最新版本侧滑界面效果实例代码
(一).前言:
这两天qq进行了重大更新(6.x)尤其在ui风格上面由之前的蓝色换成了白色居多了,侧滑效果也发生了一些变化,那我们今天来模仿实现一个qq6.x版本的侧滑界面效果。今天我们还是采用神器viewdraghelper来实现.
本次实例具体代码已经上传到下面的项目中,欢迎各位去star和fork一下。
https://github.com/jiangqqlmj/draghelper4qq
fastdev4android框架项目地址:https://github.com/jiangqqlmj/fastdev4android
(二).viewgraghelper的基本使用
前面我们学习viewgraghelper的基本使用方法,同时也知道了里边的若干个方法的用途,下面我们还是把基本的使用步骤温习一下。要使用viewgraghelper实现子view拖拽移动的步骤如下:
创建viewgraghelper实例(传入callback)
重写事件拦截处理方法onintercepttouch和ontouchevent
实现callback,实现其中的相关方法trycaptureview以及水平或者垂直方向移动的距离方法
更加具体分析大家可以看前一篇博客,或者我们今天这边会通过具体实例讲解一下。
(三).qq5.x侧滑效果实现分析:
在正式版本qq中的侧滑效果如下:
观察上面我们可以理解为两个view,一个是底部的相当于左侧功能view,另外一个是上层主功能内容view,我们在上面进行拖拽上层view或者左右滑动的时候,上层和下层的view相应进行滑动以及view大小变化,同时加入相关的动画。当然我们点击上层的view可以进行打开或者关闭侧滑菜单。
(四).侧滑效果自定义组件实现
1.首先我们这边集成自framelayout创建一个自定义view draglayout。内部的定义的一些变量如下(主要包括一些配置类,手势,viewdraghelper实例,屏幕宽高,拖拽的子视图view等)
//是否带有阴影效果 private boolean isshowshadow = true; //手势处理类 private gesturedetectorcompat gesturedetector; //视图拖拽移动帮助类 private viewdraghelper draghelper; //滑动监听器 private draglistener draglistener; //水平拖拽的距离 private int range; //宽度 private int width; //高度 private int height; //main视图距离在viewgroup距离左边的距离 private int mainleft; private context context; private imageview iv_shadow; //左侧布局 private relativelayout vg_left; //右侧(主界面布局) private customrelativelayout vg_main;
然后在内部还定义了一个回调接口主要处理拖拽过程中的一些页面打开,关闭以及滑动中的事件回调:
/** * 滑动相关回调接口 */ public interface draglistener { //界面打开 public void onopen(); //界面关闭 public void onclose(); //界面滑动过程中 public void ondrag(float percent); }
2.开始创建viewdraghelper实例,依然在自定义view draglayout初始化的时候创建,使用viewgraghelper的静态方法:
public draglayout(context context,attributeset attrs, int defstyle) { super(context, attrs, defstyle); gesturedetector = new gesturedetectorcompat(context, new yscrolldetector()); draghelper =viewdraghelper.create(this, draghelpercallback); }
其中create()方法创建的时候传入了一个draghelpercallback回调类,将会在第四点中讲到。
3.接着需要重写viewgroup中事件方法,拦截触摸事件给viewgraghelper内部进行处理,这样达到拖拽移动子view视图的目的;
/** * 拦截触摸事件 * @param ev * @return */ @override public boolean onintercepttouchevent(motionevent ev) { return draghelper.shouldintercepttouchevent(ev) &&gesturedetector.ontouchevent(ev); } /** * 将拦截的到事件给viewdraghelper进行处理 * @param e * @return */ @override public boolean ontouchevent(motionevent e){ try { draghelper.processtouchevent(e); } catch (exception ex) { ex.printstacktrace(); } return false; }
这边我们在onintercepttouchevent拦截让事件从父控件往子view中转移,然后在ontouchevent方法中拦截让viewdraghelper进行消费处理。
4.开始自定义创建viewdraghelper.callback的实例draghelpercallback分别实现一个抽象方法trycaptureview以及重写以下若干个方法来实现侧滑功能,下面一个个来看一下。
/** * 拦截所有的子view * @param child child the user is attempting to capture * @param pointerid id of the pointer attempting the capture * @return */ @override public boolean trycaptureview(viewchild, int pointerid) { return true; }
该进行拦截viewgroup(本例中为:draglayout)中所有的子view,直接返回true,表示所有的子view都可以进行拖拽移动。
/** * 水平方向移动 * @param child child view beingdragged * @param left attempted motion alongthe x axis * @param dx proposed change inposition for left * @return */ @override public int clampviewpositionhorizontal(view child, int left, int dx) { if (mainleft + dx < 0) { return 0; } else if (mainleft + dx >range) { return range; } else { return left; } }
实现该方法表示水平方向滑动,同时方法中会进行判断边界值,例如当上面的main view已经向左移动边界之外了,直接返回0,表示向左最左边只能x=0;然后向右移动会判断向右最变得距离range,至于range的初始化后边会讲到。除了这两种情况之外,就是直接返回left即可。
/** * 设置水平方向滑动的最远距离 *@param child child view to check 屏幕宽度 * @return */ @override public int getviewhorizontaldragrange(view child) { return width; }
该方法有必要实现,因为该方法在callback内部默认返回0,也就是说,如果的view的click事件为true,那么会出现整个子view没法拖拽移动的情况了。那么这边直接返回left view宽度了,表示水平方向滑动的最远距离了。
/** * 当拖拽的子view,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭 * @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) { close(); } else if (releasedchild == vg_main&& mainleft > range * 0.3) { open(); } else if (releasedchild == vg_left&& mainleft > range * 0.7) { open(); } else { close(); } }
该方法在拖拽子view移动手指释放的时候被调用,这是会判断移动向左,向右的意图,进行打开或者关闭man view(上层视图)。下面是实现的最后一个方法:onviewpositionchanged
/** * 子view被拖拽 移动的时候回调的方法 * @param changedview view whoseposition changed * @param left new x coordinate of theleft edge of the view * @param top new y coordinate of thetop edge of the view * @param dx change in x position fromthe last call * @param dy change in y position fromthe last call */ @override public void onviewpositionchanged(view changedview, int left, int top, int dx, int dy) { if (changedview == vg_main) { mainleft = left; } else { mainleft = mainleft + left; } if (mainleft < 0) { mainleft = 0; } else if (mainleft > range) { mainleft = range; } if (isshowshadow) { iv_shadow.layout(mainleft, 0,mainleft + width, height); } if (changedview == vg_left) { vg_left.layout(0, 0, width,height); vg_main.layout(mainleft, 0,mainleft + width, height); } dispatchdragevent(mainleft); } };
该方法是在我们进行拖拽移动子view的过程中进行回调,根据移动坐标位置,然后进行重新定义left view和main view。同时调用dispathdragevent()方法进行拖拽事件相关处理分发同时根据状态来回调接口:
/** * 进行处理拖拽事件 * @param mainleft */ private void dispatchdragevent(int mainleft) { if (draglistener == null) { return; } float percent = mainleft / (float)range; //根据滑动的距离的比例 animateview(percent); //进行回调滑动的百分比 draglistener.ondrag(percent); status laststatus = status; if (laststatus != getstatus()&& status == status.close) { draglistener.onclose(); } else if (laststatus != getstatus()&& status == status.open) { draglistener.onopen(); } }
该方法中有一行代码float percent=mainleft/(float)range;算到一个百分比后面会用到
5.至于子view布局的获取初始化以及宽高和水平滑动距离的大小设置方法:
/** * 布局加载完成回调 * 做一些初始化的操作 */ @override protected void onfinishinflate() { super.onfinishinflate(); if (isshowshadow) { iv_shadow = new imageview(context); iv_shadow.setimageresource(r.mipmap.shadow); layoutparams lp = new layoutparams(layoutparams.match_parent, layoutparams.match_parent); addview(iv_shadow, 1, lp); } //左侧界面 vg_left = (relativelayout)getchildat(0); //右侧(主)界面 vg_main = (customrelativelayout)getchildat(isshowshadow ? 2 : 1); vg_main.setdraglayout(this); vg_left.setclickable(true); vg_main.setclickable(true); }
以及控件大小发生变化回调的方法:
@override protected void onsizechanged(int w, int h,int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); width = vg_left.getmeasuredwidth(); height = vg_left.getmeasuredheight(); //可以水平拖拽滑动的距离 一共为屏幕宽度的80% range = (int) (width *0.8f); }
在该方法中我们可以实时获取宽和高以及拖拽水平距离。
6.上面的所有核心代码都为使用viewdraghelper实现子控件view拖拽移动的方法,但是根据我们这边侧滑效果还需要实现动画以及滑动过程中view的缩放效果,所以我们这边引入了一个动画开源库:
然后根据前面算出来的百分比来缩放view视图:
/** * 根据滑动的距离的比例,进行平移动画 * @param percent */ private void animateview(float percent) { float f1 = 1 - percent * 0.5f; viewhelper.settranslationx(vg_left,-vg_left.getwidth() / 2.5f + vg_left.getwidth() / 2.5f * percent); if (isshowshadow) { //阴影效果视图大小进行缩放 viewhelper.setscalex(iv_shadow, f1* 1.2f * (1 - percent * 0.10f)); viewhelper.setscaley(iv_shadow, f1* 1.85f * (1 - percent * 0.10f)); } }
7.当然除了上面这些还缺少一个效果就是,当我们滑动过程中假如我们手指释放,按照常理来讲view就不会在进行移动了,那么这边我们需要一个加速度当我们释放之后,还能保持一定的速度,该怎么样实现呢?答案就是实现computescroll()方法。
/** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @override public void computescroll() { if (draghelper.continuesettling(true)){ viewcompat.postinvalidateonanimation(this); } }
ok上面关于draglayout的核心代码就差不多这么多了,下面是使用draglayout类来实现侧滑效果啦!
(五).侧滑效果组件使用
1.首先使用的布局文件如下:
<com.chinaztt.widget.draglayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/dl" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent" > <!--下层 左边的布局--> <includelayoutincludelayout="@layout/left_view_layout"/> <!--上层 右边的主布局--> <com.chinaztt.widget.customrelativelayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" > <linearlayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <relativelayout android:id="@+id/rl_title" android:layout_width="match_parent" android:layout_height="49dp" android:gravity="bottom" android:background="@android:color/holo_orange_light" > <includelayoutincludelayout="@layout/common_top_bar_layout"/> </relativelayout> <!--中间内容后面放入fragment--> <framelayout android:layout_width="fill_parent" android:layout_height="fill_parent" > <fragment android:id="@+id/main_info_fragment" class="com.chinaztt.fragment.onefragment" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </framelayout> </linearlayout> </com.chinaztt.widget.customrelativelayout> </com.chinaztt.widget.draglayout>
该布局文件中父层view就是draglayout,然后内部有两个relativelayout布局,分别充当下一层布局和上一层主布局。
2.下面我们来看一下下层菜单布局,这边我专门写了一个left_view_layout.xml文件,其中主要分为三块,第一块顶部为头像个人基本信息布局,中间为功能入口列表,底部是设置等功能,具体布局代码如下:
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingtop="70dp" android:background="@drawable/sidebar_bg" > <linearlayout android:id="@+id/ll1" android:paddingleft="30dp" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical"> <!--头像,昵称信息--> <linearlayout android:layout_width="match_parent" android:layout_height="70dp" android:orientation="horizontal" android:gravity="center_vertical" > <com.chinaztt.widget.roundangleimageview android:id="@+id/iv_bottom" android:layout_width="50dp" android:layout_height="50dp" android:scaletype="fitxy" android:src="@drawable/icon_logo" app:roundwidth="25dp" app:roundheight="25dp"/> <linearlayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:layout_gravity="center_vertical" android:orientation="vertical"> <relativelayout android:layout_width="fill_parent" android:layout_height="wrap_content" > <textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centervertical="true" android:layout_marginleft="15dp" android:text="名字:jiangqqlmj" android:textcolor="@android:color/black" android:textsize="15sp" /> <imagebutton android:layout_alignparentright="true" android:layout_centervertical="true" android:layout_marginright="100dp" android:layout_width="22dp" android:layout_height="22dp" android:background="@drawable/qrcode_selector"/> </relativelayout> <textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginleft="15dp" android:text="qq:781931404" android:textcolor="@android:color/black" android:textsize="13sp" /> </linearlayout> </linearlayout> <linearlayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal"> <imageview android:layout_width="17dp" android:layout_height="17dp" android:scaletype="fitxy" android:src="@drawable/sidebar_signature_nor"/> <textview android:layout_marginleft="5dp" android:textsize="13sp" android:textcolor="#676767" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="用心做产品!"/> </linearlayout> </linearlayout> <!--底部功能条--> <includelayoutincludelayout="@layout/left_view_bottom_layout" android:id="@+id/bottom_view" /> <!--中间列表--> <listview android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/bottom_view" android:layout_below="@id/ll1" android:layout_marginbottom="30dp" android:layout_margintop="70dp" android:cachecolorhint="#00000000" android:listselector="@drawable/lv_click_selector" android:divider="@null" android:scrollbars="none" android:textcolor="#ffffff"/> </relativelayout>
该布局还是比较简单的,对于上层主内容布局这边就放一个顶部导航栏和中的fragment内容信息,留着后期大家功能扩展即可。
3.主activity使用如下:
public class mainactivity extends baseactivity { private draglayout dl; private listview lv; private imageview iv_icon, iv_bottom; private quickadapter<itembean> quickadapter; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); setstatusbar(); initdraglayout(); initview(); } private void initdraglayout() { dl = (draglayout) findviewbyid(r.id.dl); dl.setdraglistener(new draglayout.draglistener() { //界面打开的时候 @override public void onopen() { } //界面关闭的时候 @override public void onclose() { } //界面滑动的时候 @override public void ondrag(float percent) { viewhelper.setalpha(iv_icon, 1 - percent); } }); } private void initview() { iv_icon = (imageview) findviewbyid(r.id.iv_icon); iv_bottom = (imageview) findviewbyid(r.id.iv_bottom); lv = (listview) findviewbyid(r.id.lv); lv.setadapter(quickadapter=new quickadapter<itembean>(this,r.layout.item_left_layout, itemdatautils.getitembeans()) { @override protected void convert(baseadapterhelper helper, itembean item) { helper.setimageresource(r.id.item_img,item.getimg()) .settext(r.id.item_tv,item.gettitle()); } }); lv.setonitemclicklistener(new onitemclicklistener() { @override public void onitemclick(adapterview<?> arg0, view arg1, int position, long arg3) { toast.maketext(mainactivity.this,"click item "+position,toast.length_short).show(); } }); iv_icon.setonclicklistener(new onclicklistener() { @override public void onclick(view arg0) { dl.open(); } }); } }
初始化控件,设置滑动监听器,以及左侧菜单功能列表设置即可了,不过上面大家应该看了quickadapter的使用,该为baseadapterhelper框架使用,我们需要在项目build.gradle中作如下配置:
具体关于baseadapter的使用讲解博客地址如下:
4.正式运行效果如下:
5.因为这边底层需要viewdraghelper类,所以大家在使用的时候需要导入v4包的,不过我这边直接把viewgraghelper类的源代码复制到项目中了。
(六).draglayout源代码带注释
上面主要分析draglayout的具体实现,不过我这边也贴一下draglayout带有注释的全部源代码让大家可以更好的了解draglayout的具体实现代码:
/** *使用viewrraghelper实现侧滑效果功能 */ publicclass draglayout extends framelayout { private boolean isshowshadow = true; //手势处理类 private gesturedetectorcompat gesturedetector; //视图拖拽移动帮助类 private viewdraghelper draghelper; //滑动监听器 private draglistener draglistener; //水平拖拽的距离 private int range; //宽度 private int width; //高度 private int height; //main视图距离在viewgroup距离左边的距离 private int mainleft; private context context; private imageview iv_shadow; //左侧布局 private relativelayout vg_left; //右侧(主界面布局) private customrelativelayout vg_main; //页面状态 默认为关闭 private status status = status.close; public draglayout(context context) { this(context, null); } public draglayout(context context,attributeset attrs) { this(context, attrs, 0); this.context = context; } public draglayout(context context,attributeset attrs, int defstyle) { super(context, attrs, defstyle); gesturedetector = new gesturedetectorcompat(context, new yscrolldetector()); draghelper =viewdraghelper.create(this, draghelpercallback); } class yscrolldetector extends simpleongesturelistener { @override public boolean onscroll(motionevent e1,motionevent e2, float dx, float dy) { return math.abs(dy) <=math.abs(dx); } } /** * 实现子view的拖拽滑动,实现callback当中相关的方法 */ private viewdraghelper.callback draghelpercallback = new viewdraghelper.callback() { /** * 水平方向移动 * @param child child view beingdragged * @param left attempted motion alongthe x axis * @param dx proposed change inposition for left * @return */ @override public int clampviewpositionhorizontal(view child, int left, int dx) { if (mainleft + dx < 0) { return 0; } else if (mainleft + dx >range) { return range; } else { return left; } } /** * 拦截所有的子view * @param child child the user isattempting to capture * @param pointerid id of the pointerattempting the capture * @return */ @override public boolean trycaptureview(view child, int pointerid) { return true; } /** * 设置水平方向滑动的最远距离 *@param child child view to check 屏幕宽度 * @return */ @override public int getviewhorizontaldragrange(view child) { return width; } /** * 当拖拽的子view,手势释放的时候回调的方法, 然后根据左滑或者右滑的距离进行判断打开或者关闭 * @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) { close(); } else if (releasedchild == vg_main&& mainleft > range * 0.3) { open(); } else if (releasedchild == vg_left&& mainleft > range * 0.7) { open(); } else { close(); } } /** * 子view被拖拽 移动的时候回调的方法 * @param changedview view whoseposition changed * @param left new x coordinate of theleft edge of the view * @param top new y coordinate of thetop edge of the view * @param dx change in x position fromthe last call * @param dy change in y position fromthe last call */ @override public void onviewpositionchanged(view changedview, int left, int top, int dx, int dy) { if (changedview == vg_main) { mainleft = left; } else { mainleft = mainleft + left; } if (mainleft < 0) { mainleft = 0; } else if (mainleft > range) { mainleft = range; } if (isshowshadow) { iv_shadow.layout(mainleft, 0,mainleft + width, height); } if (changedview == vg_left) { vg_left.layout(0, 0, width,height); vg_main.layout(mainleft, 0,mainleft + width, height); } dispatchdragevent(mainleft); } }; /** * 滑动相关回调接口 */ public interface draglistener { //界面打开 public void onopen(); //界面关闭 public void onclose(); //界面滑动过程中 public void ondrag(float percent); } public void setdraglistener(draglistener draglistener) { this.draglistener = draglistener; } /** * 布局加载完成回调 * 做一些初始化的操作 */ @override protected void onfinishinflate() { super.onfinishinflate(); if (isshowshadow) { iv_shadow = new imageview(context); iv_shadow.setimageresource(r.mipmap.shadow); layoutparams lp = new layoutparams(layoutparams.match_parent, layoutparams.match_parent); addview(iv_shadow, 1, lp); } //左侧界面 vg_left = (relativelayout)getchildat(0); //右侧(主)界面 vg_main = (customrelativelayout)getchildat(isshowshadow ? 2 : 1); vg_main.setdraglayout(this); vg_left.setclickable(true); vg_main.setclickable(true); } public viewgroup getvg_main() { return vg_main; } public viewgroup getvg_left() { return vg_left; } @override protected void onsizechanged(int w, int h,int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); width = vg_left.getmeasuredwidth(); height = vg_left.getmeasuredheight(); //可以水平拖拽滑动的距离 一共为屏幕宽度的80% range = (int) (width * 0.8f); } /** * 调用进行left和main 视图进行位置布局 * @param changed * @param left * @param top * @param right * @param bottom */ @override protected void onlayout(boolean changed,int left, int top, int right, int bottom) { vg_left.layout(0, 0, width, height); vg_main.layout(mainleft, 0, mainleft +width, height); } /** * 拦截触摸事件 * @param ev * @return */ @override public boolean onintercepttouchevent(motionevent ev) { returndraghelper.shouldintercepttouchevent(ev) &&gesturedetector.ontouchevent(ev); } /** * 将拦截的到事件给viewdraghelper进行处理 * @param e * @return */ @override public boolean ontouchevent(motionevent e){ try { draghelper.processtouchevent(e); } catch (exception ex) { ex.printstacktrace(); } return false; } /** * 进行处理拖拽事件 * @param mainleft */ private void dispatchdragevent(intmainleft) { if (draglistener == null) { return; } float percent = mainleft / (float)range; //滑动动画效果 animateview(percent); //进行回调滑动的百分比 draglistener.ondrag(percent); status laststatus = status; if (laststatus != getstatus()&& status == status.close) { draglistener.onclose(); } else if (laststatus != getstatus()&& status == status.open) { draglistener.onopen(); } } /** * 根据滑动的距离的比例,进行平移动画 * @param percent */ private void animateview(float percent) { float f1 = 1 - percent * 0.5f; viewhelper.settranslationx(vg_left,-vg_left.getwidth() / 2.5f + vg_left.getwidth() / 2.5f * percent); if (isshowshadow) { //阴影效果视图大小进行缩放 viewhelper.setscalex(iv_shadow, f1* 1.2f * (1 - percent * 0.10f)); viewhelper.setscaley(iv_shadow, f1* 1.85f * (1 - percent * 0.10f)); } } /** * 有加速度,当我们停止滑动的时候,该不会立即停止动画效果 */ @override public void computescroll() { if (draghelper.continuesettling(true)){ viewcompat.postinvalidateonanimation(this); } } /** * 页面状态(滑动,打开,关闭) */ public enum status { drag, open, close } /** * 页面状态设置 * @return */ public status getstatus() { if (mainleft == 0) { status = status.close; } else if (mainleft == range) { status = status.open; } else { status = status.drag; } return status; } public void open() { open(true); } public void open(boolean animate) { if (animate) { //继续滑动 if(draghelper.smoothslideviewto(vg_main, range, 0)) { viewcompat.postinvalidateonanimation(this); } } else { vg_main.layout(range, 0, range * 2,height); dispatchdragevent(range); } } public void close() { close(true); } public void close(boolean animate) { if (animate) { //继续滑动 if(draghelper.smoothslideviewto(vg_main, 0, 0)) { viewcompat.postinvalidateonanimation(this); } } else { vg_main.layout(0, 0, width,height); dispatchdragevent(0); } } }
(七).最后总结
今天我们实现打造qq最新版本qq6.x效果,同时里边用到了viewdraghelper,baseadapterhelper的运用,具体该知识点的使用方法,我已经在我的博客中更新讲解的文章,欢迎大家查看。
上一篇: Java定时任务的三种实现方式