Android自定义ViewGroup(侧滑菜单)详解及简单实例
自定义侧滑菜单的简单实现
不少app中都有这种侧滑菜单,例如qq这类的,比较有名开源库如slidingmenu。
有兴趣的可以去研究研究这个开源库。
这里我们将一种自己的实现方法,把学习的 东西做个记录,o(∩_∩)o!
首先看效果图:
这里我们实现的侧滑菜单,是将左侧隐藏的菜单和主面板看作一个整体来实现的,而左侧隐藏的菜单和主面板相当于是这个自定义view的子view。
首先来构造该自定义view的布局:
自定义的slidemenuview包含两个子view,一个是menuview,另一个是mainview(主面板)。
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="${relativepackage}.${activityclass}" > <com.wind.view.slidemenuview android:id="@+id/slidemenu" android:layout_width="match_parent" android:layout_height="match_parent" > <include layout="@layout/layout_menu"/> <include layout="@layout/layout_main"/> </com.wind.view.slidemenuview> </relativelayout>
接着我们需要实现slidemenuview的 java代码。
自定义view,需要继承某个父类,通过重写父类的某些方法来增强父类的功能。
这里我们选择继承viewgroup,一般自定义view,需要重写onmeasure,onlayout和ondraw,正常情况下只要重写ondraw就可以了,特殊情况下,需要重写onmeasure 和onlayout。
package com.wind.view; import android.content.context; import android.util.attributeset; import android.util.log; import android.view.motionevent; import android.view.view; import android.view.viewgroup; import android.widget.scroller; public class slidemenuview extends viewgroup { // framelayout { private static final string tag = "slidemenuview"; private view menuview, mainview; private int menuwidth = 0; private scroller scroller; public slidemenuview(context context) { super(context); init(); } public slidemenuview(context context, attributeset attrs) { super(context, attrs); init(); } private void init() { scroller = new scroller(getcontext()); } /** * 当一级的子view全部加载玩后调用,可以用于初始化子view的引用 * */ @override protected void onfinishinflate() { super.onfinishinflate(); menuview = getchildat(0); mainview = getchildat(1); menuwidth = menuview.getlayoutparams().width; log.d(tag, "onfinishinflate() menuwidth: " + menuwidth); } /** * widthmeasurespec和heightmeasurespec是系统测量slidemenu时传入的参数, * 这2个参数测量出的宽高能让slidemenu充满窗体,其实是正好等于屏幕宽高 */ @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); log.d(tag, "onmeasure: widthmeasurespec: " + widthmeasurespec + "heightmeasurespec: " + heightmeasurespec); // int measurespec = measurespec.makemeasurespec(menuwidth, // measurespec.exactly); // // log.d(tag,"onmeasure: measurespec: " +measurespec); // 测量所有子view的宽高 // 通过getlayoutparams方法可以获取到布局文件中指定宽高 menuview.measure(widthmeasurespec, heightmeasurespec); // 直接使用slidemenu的测量参数,因为它的宽高都是充满父窗体 mainview.measure(widthmeasurespec, heightmeasurespec); } /** * 重新摆放子view的位置 * l: 当前子view的左边在父view的坐标系中的x坐标 * t: 当前子view的顶边在父view的坐标系中的y坐标 */ @override protected void onlayout(boolean changed, int l, int t, int r, int b) { log.d(tag, "onlayout() changed: " + changed + "l: " + l + "t: " + t + "r: " + r + "b: " + b); log.d(tag,"menuview.getmeasuredheight(): " + menuview.getmeasuredheight()); menuview.layout(-menuwidth, 0, 0, menuview.getmeasuredheight()); mainview.layout(0, 0, r, b); } private int downx; @override public boolean ontouchevent(motionevent event) { switch (event.getaction()) { case motionevent.action_down: downx = (int) event.getx(); break; case motionevent.action_move: int movex = (int) event.getx(); int deltax = (movex - downx); log.e(tag, "scrollx: " + getscrollx()); int newscrollx = getscrollx() - deltax; //使得slidemenuview不会滑动出界 if(newscrollx > 0) newscrollx = 0; if(newscrollx < -menuwidth) newscrollx = -menuwidth; scrollto(newscrollx, 0); log.e(tag, "movex: " + movex); downx = movex; break; case motionevent.action_up: //①.使用自定义动画 // scrollanimation scrollanimation; // if(getscrollx()>-menuwidth/2){ // //关闭菜单 //// scrollto(0, 0); // scrollanimation = new scrollanimation(this, 0); // }else { // //打开菜单 //// scrollto(-menuwidth, 0); // scrollanimation = new scrollanimation(this, -menuwidth); // } // startanimation(scrollanimation); //②使用scroller if(getscrollx()>-menuwidth/2){ // //关闭菜单 closemenu(); }else { //打开菜单 openmenu(); } break; } return true; } private void openmenu() { log.d(tag, "openmenu..."); scroller.startscroll(getscrollx(), 0, -menuwidth-getscrollx(), 400); invalidate(); } private void closemenu() { log.d(tag, "closemenu..."); scroller.startscroll(getscrollx(), 0, 0-getscrollx(), 400); invalidate(); } /** * scroller不主动去调用这个方法 * 而invalidate()可以掉这个方法 * invalidate->draw->computescroll */ @override public void computescroll() { super.computescroll(); if(scroller.computescrolloffset()){//返回true,表示动画没结束 scrollto(scroller.getcurrx(), 0); invalidate(); } } //用于在activity中来控制菜单的状态 public void switchmenu() { if(getscrollx() == 0) { openmenu(); } else { closemenu(); } } }
- 这里继承viewgroup,由于子view都是利用系统的控件填充,所以不需要重写ondraw方法。
- 由于是继承自viewgroup,需要实现onmeasure来测量两个子view的高度和宽度。我们还可以继承framelayout,则不需要实现onmeasure,因为framelayout已经帮我们实现onmeasure。
- 重写onlayout方法,重新定义自定义的位置摆放,左侧的侧滑菜单需要使其处于隐藏状态。
- 重写ontouch方法,通过监测touch的up和down来滑动view来实现侧滑的效果。
关于平滑滚动
我们可以采用scroller类中的startscroll来实现平滑滚动,同样我们可以使用自定义动画的方式来实现。
package com.wind.view; import android.view.view; import android.view.animation.animation; import android.view.animation.transformation; /** * 让指定view在一段时间内scrollto到指定位置 * @author administrator * */ public class scrollanimation extends animation{ private view view; private int targetscrollx; private int startscrollx; private int totalvalue; public scrollanimation(view view, int targetscrollx) { super(); this.view = view; this.targetscrollx = targetscrollx; startscrollx = view.getscrollx(); totalvalue = this.targetscrollx - startscrollx; int time = math.abs(totalvalue); setduration(time); } /** * 在指定的时间内一直执行该方法,直到动画结束 * interpolatedtime:0-1 标识动画执行的进度或者百分比 * time : 0 - 0.5 - 0.7 - 1 * value: 10 - 60 - 80 - 110 * 当前的值 = 起始值 + 总的差值*interpolatedtime */ @override protected void applytransformation(float interpolatedtime, transformation t) { super.applytransformation(interpolatedtime, t); int currentscrollx = (int) (startscrollx + totalvalue*interpolatedtime); view.scrollto(currentscrollx, 0); } }
如上面的代码:
通过自定义动画来让view在一段时间内重复执行这个动作。
关于getscrollx
将view向右移动的时候,通过view.getscrollx得到的值是负的。
其实可以这样理解:
*getscrollx()表示的是当前的屏幕x坐标的最小值-移动的距离(向右滑动时移动的距离为正值,
向左滑动时移动的距离为负值)。*
对scrollto和scrollby的理解
我们查看view的源码发现,scrollby其实调用的就是scrollto,scrollto就是把view移动到屏幕的x和y位置,也就是绝对位置。而scrollby其实就是调用的scrollto,但是参数是当前mscrollx和mscrolly加上x和y的位置,所以scrollby调用的是相对于mscrollx和mscrolly的位置。
我们在上面的代码中可以看到当我们手指不放移动屏幕时,就会调用scrollby来移动一段相对的距离。而当我们手指松开后,会调用mscroller.startscroll(munboundedscrollx, 0, delta, 0, duration);来产生一段动画来移动到相应的页面,在这个过程中系统回不断调用computescroll(),我们再使用scrollto来把view移动到当前scroller所在的绝对位置。
/** * set the scrolled position of your view. this will cause a call to * {@link #onscrollchanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollto(int x, int y) { if (mscrollx != x || mscrolly != y) { int oldx = mscrollx; int oldy = mscrolly; mscrollx = x; mscrolly = y; invalidateparentcaches(); onscrollchanged(mscrollx, mscrolly, oldx, oldy); if (!awakenscrollbars()) { invalidate(true); } } } /** * move the scrolled position of your view. this will cause a call to * {@link #onscrollchanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollby(int x, int y) { scrollto(mscrollx + x, mscrolly + y); }
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!