在自定义控件——初识自定义控件里面,我们已经对自定义控件进行描述和分类。其分类分别是
- 自制控件
- 组合控件
- 拓展控件
这篇博文里面,我们继续进行自制控件。
我们想要继续的是一个简单的仿造qq侧滑菜单。
先来看一下效果图
在(初识自定义控件)中,我们知道了自定义控件分为三种
- 自制控件
- 组合控件
- 拓展控件
在(自制控件1)中,我们自制了一个开关按钮View,这次,我们来做自制的ViewGroup,一个简单的仿qq策划菜单。
在(自制控件1)我们利用View.layout(l,t,r,b)这个api让View动起来。在本次的侧滑菜单里面,我们使用
ScrollTo和ScrollBy让View动起来
而且使用Scroller做弹性滑动。
如果对自制继承自ViewGroup的控件还没有一个大概的概念,可以通过(初识自定义控件)这篇博文里面的demo,进行一个大概的了解。
一、造起来一个ViewGroup
新建一个类,比如叫做SlideMenu,继承自ViewGroup
public class SlideMenu extends ViewGroup{
public SlideMenu(Context context) {
super(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
.
.
然后我们在 activity_main 利用控件的全路径名引入这个控件
<?xml version="1.0" encoding="utf-8"?>
<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="com.amqr.diyslidemenu.MainActivity">
<com.amqr.diyslidemenu.view.SlideMenu
android:id="@+id/mSmenu"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
</com.amqr.diyslidemenu.view.SlideMenu>
</RelativeLayout>
.
.
二、弄两个布局文件,一个左侧菜单的,一个是主页部分的。SlideView里面把这两个布局加载出来
.
1、准备两个布局文件,左侧菜单的布局文件需要控制宽度,这里我们设置为200dp
左侧菜单 slide_left.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="200dp"
android:layout_height="match_parent"
android:background="#ff0000"
>
<!--左侧的菜单限定为200dp-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="本地"
android:textSize="26sp"
android:gravity="center"
android:layout_marginTop="10dp"
android:background="#689342"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="体育"
android:textSize="26sp"
android:gravity="center"
android:layout_marginTop="10dp"
android:background="#689342"
/>
</LinearLayout>
</ScrollView>
主页部分 slide_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="主页内容区域"
android:layout_gravity="center"
android:textSize="30dp"
android:layout_centerInParent="true"
/>
</RelativeLayout>
2、引用xml的布局代码里面include进来左侧菜单和布局文件
这个include的先后顺序需要严格区分
因为待会需要结合SlideView的onFinishInflate相互结合
<?xml version="1.0" encoding="utf-8"?>
<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="com.amqr.diyslidemenu.MainActivity">
<com.amqr.diyslidemenu.view.SlideMenu
android:id="@+id/mSmenu"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<!--这个include的先后顺序需要严格区分,
因为待会需要结合SlideView的onFinishInflate相互结合 -->
<!--左侧菜单-->
<include layout="@layout/slide_left"/>
<!--主页部分-->
<include layout="@layout/slide_main"/>
</com.amqr.diyslidemenu.view.SlideMenu>
</RelativeLayout>
三、利用SlideView的onFinishInflate方法加载view
利用SlideView的onFinishInflate方法加载view
public class SlideMenu extends ViewGroup{
private View mLeftMenu;
private View mMainPage;
public SlideMenu(Context context) {
super(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 当SlideView被xml引用加载之后完成,这个方法就会调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate(); // ???
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
}
}
看一下onFinishInflate这个方法,属于View类下一个方法
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*
* <p>Even if the subclass overrides onFinishInflate, they should always be
* sure to call the super method, so that we get called.
*/
@CallSuper
protected void onFinishInflate() {
}
getChildAt的作用是 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
@Override
protected void onFinishInflate() {
super.onFinishInflate(); // ???
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
}
四、利用onMeasure来孩子测量大小
首先记住,不管干嘛,首先先把现在onMeasure里面把setMeasuredDimension方法给写上。
说在测量之前的第一点
我们的ViewGroup也是View这点我们都知道,其实到最终,ViewGroup到最后还是给他的父亲调用,他的父亲就是使用使用measure来测量ViewGroup的大小的。说了这么多,我们还是看一下代码吧。
<?xml version="1.0" encoding="utf-8"?>
<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="com.amqr.diyslidemenu.MainActivity">
<!--这个ViewGroup,他的父亲就是上面的RelativeLayout,RelativeLayout就是使用measure来测量这个SlideView的大小的-->
<com.amqr.diyslidemenu.view.SlideMenu
android:id="@+id/mSmenu"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<!--这个include的先后顺序需要严格区分,
因为待会需要结合SlideView的onFinishInflate相互结合 -->
<!--左侧菜单-->
<include layout="@layout/slide_left"/>
<!--主页部分-->
<include layout="@layout/slide_main"/>
</com.amqr.diyslidemenu.view.SlideMenu>
</RelativeLayout>
这个SlideMenu是ViewGroup,他的父亲就是上面的RelativeLayout,RelativeLayout就是使用measure来测量这个SlideView的大小的
明白了onMeasure是给measure调用的之后,我们就应该清楚地知道,onMeasure是父亲给孩子用的宽高(父亲把自己所能给的都给了,也就是最大的,我们可以采用父亲的宽高,我们可以自己指定宽高。但是孩子的自由发挥的空间没有办法超出父亲所能给的最大值,但是可以比父亲小)
说在测量之前的第二点
怎么得到一个View在xml布局文件里面宽?
利用view.getLayoutParams().width,高类似
有了前面的两点说明,现在我们可以真正式在SlideMenu里面复写onMeasere方法并且进行测量了
代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// makeMeasureSpec 的时候,第一个参数是大小,第二个参数是模式
// 宽度我们使用左侧菜单的在xml里面的200dp,因为指定大小所以模式是MeasureSpec.EXACTLY
// 怎么得到一个View在xml布局文件里面宽,利用view.getLayoutParams().width,高类似
int leftViewMeasureSpecWidth = MeasureSpec.
makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
//左侧菜单的的高度我们希望填充父窗体,而当前onMeasure里面的heightMeasureSpec根据我们的布局显然就是填充父窗体
// 所以一直接用父亲传过来的这个32位参数就好
mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
// 至于主页页面,我们的希望他宽高都是填充父窗体,所以直接用onMeasure里面传过来的参数就好啦
mMainPage.measure(widthMeasureSpec,heightMeasureSpec);
// onMeasure一开始什么都不管就应该复写setMeasuredDimension,不然报错。
// 除非我们的自定义控件是宽高都是填充父窗体,那么我们就留着下面这句super的代码就可以
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
}
关于测量的View的measure方法的参数可以参考(初识自定义控件)
五、利用onLayout给自孩子摆放位置
在onMeasure里面给孩子布局采用的方法是 child.measure方法
利用onLayout给自己孩子摆放位置相应来说用的是 child.layout方法
摆放之前,了解getMeasuredWidth();和getWidth()的区别
我们下面说的layout的前提是已经进行onMeasure被执行之后(onMeasure里面必须执行setMeasuredDimension)
getWidth()必须在控件的 layout(l,t,r,b) 被执行过后才能获取到有效的值,也就在View被绘制好之后才有效
getMeasuredWidth(); 是 layout(l,t,r,b)执行之前就会获取到View的宽度的,也就是在View被绘制好之前就可以生效的。
先测量,后摆放。
也就是 先onMeasure,后onLayout。
有了这些了解,我们可以来很好地摆放位置了
代码如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftViewWidth = mLeftMenu.getMeasuredWidth();
int leftViewHeight = mLeftMenu.getMeasuredHeight();
// l t r b 左上右下 左上一点,右下一点,两点确定了一个矩形的大小
mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
int mainViewWidth = mMainPage.getMeasuredWidth();
int mainViewHeight = mMainPage.getMeasuredHeight();
mMainPage.layout(0,0,mainViewWidth,mainViewHeight);
}
到此为止先停一下,看一下SlideView里面目前的代码:
SlideMenu
public class SlideMenu extends ViewGroup{
private View mLeftMenu;
private View mMainPage;
public SlideMenu(Context context) {
super(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 当SlideView被xml引用加载之后完成,这个方法就会调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate(); // ???
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// makeMeasureSpec 的时候,第一个参数是大小,第二个参数是模式
// 宽度我们使用左侧菜单的在xml里面的200dp,因为指定大小所以模式是MeasureSpec.EXACTLY
// 怎么得到一个View在xml布局文件里面宽,利用view.getLayoutParams().width,高类似
int leftViewMeasureSpecWidth = MeasureSpec.
makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
//左侧菜单的的高度我们希望填充父窗体,而当前onMeasure里面的heightMeasureSpec根据我们的布局显然就是填充父窗体
// 所以一直接用父亲传过来的这个32位参数就好
mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
// 至于主页页面,我们的希望他宽高都是填充父窗体,所以直接用onMeasure里面传过来的参数就好啦
mMainPage.measure(widthMeasureSpec,heightMeasureSpec);
// onMeasure一开始什么都不管就应该复写setMeasuredDimension,不然报错。
// 除非我们的自定义控件是宽高都是填充父窗体,那么我们就留着下面这句super的代码就可以
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftViewWidth = mLeftMenu.getMeasuredWidth();
int leftViewHeight = mLeftMenu.getMeasuredHeight();
Log.d("Slide","getWidth:"+mLeftMenu.getWidth()+" mLeftMenu.getHeight():"+mLeftMenu.getHeight());
Log.d("Slide","getMeasuredWidth:"+mLeftMenu.getMeasuredWidth()+" getMeasuredHeight:"+mLeftMenu.getMeasuredHeight());
// l t r b 左上右下 左上一点,右下一点,两点确定了一个矩形的大小
mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
int mainViewWidth = mMainPage.getMeasuredWidth();
int mainViewHeight = mMainPage.getMeasuredHeight();
mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
}
}
MainActivity
package com.amqr.diyslidemenu;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
运行效果:
当前还无法拉动,但是我们已经过能让界面显示出来了。
六、开始做移动效果
移动的方式有很多种,这次我们采用的是ScrollTo+ScrollBy方式
说明1: ScrollTo和ScrollBy 的了解
ScrollTo和ScrollBy就是手机屏幕的左上角动,而不是View或者ViewGroup动。
区别是,ScrollTo每次都是都是想比较于最开始的左上角(0,0)
ScrollBy每次的移动是累计的
比如,调用ScollTo(20,0),的时候,那么手机屏幕回向右移动20个单位,但是再次调用ScollTo(20,0),的时候是不动的,因为每次都是跟最开始的(0,0)做比较;然后我们调用ScrollTo(-20,0)的时候,就回到最开的原点。
ScrollBy是累计的,第一次调用ScrollBy(20,0),向右移动20 个单位,再次调用ScrollBy(20,0),那么屏幕的左上角就会移动到(40,0)的位置,因为累计嘛。
说明2: getX和getRawX的了解
getX()是触摸的点与控件自身的距离
getRawX()是触摸的点与屏幕的距离
结论:当你触到按钮时,x,y是相对于该按钮左上点(控件本身)的相对位置。而rawx,rawy始终是相对于屏幕的位置。
说明3:getScrollX()和getScrollY()的了解
getScrollX(): 手机屏幕显示区域左上角 与 你指定的View的左上角的横向距离getScrollY(): 手机屏幕显示区域左上角 与 你指定的View的左上角的垂直距离(因为子视图的高度和手机屏幕高度一样)
六.1、简单的移动,不会产生越界现象
public class SlideMenu extends ViewGroup{
private View mLeftMenu;
private View mMainPage;
private int downX;
public SlideMenu(Context context) {
super(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 当SlideView被xml引用加载之后完成,这个方法就会调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate(); // ???
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// makeMeasureSpec 的时候,第一个参数是大小,第二个参数是模式
// 宽度我们使用左侧菜单的在xml里面的200dp,因为指定大小所以模式是MeasureSpec.EXACTLY
// 怎么得到一个View在xml布局文件里面宽,利用view.getLayoutParams().width,高类似
int leftViewMeasureSpecWidth = MeasureSpec.
makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
//左侧菜单的的高度我们希望填充父窗体,而当前onMeasure里面的heightMeasureSpec根据我们的布局显然就是填充父窗体
// 所以一直接用父亲传过来的这个32位参数就好
mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
// 至于主页页面,我们的希望他宽高都是填充父窗体,所以直接用onMeasure里面传过来的参数就好啦
mMainPage.measure(widthMeasureSpec,heightMeasureSpec);
// onMeasure一开始什么都不管就应该复写setMeasuredDimension,不然报错。
// 除非我们的自定义控件是宽高都是填充父窗体,那么我们就留着下面这句super的代码就可以
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftViewWidth = mLeftMenu.getMeasuredWidth();
int leftViewHeight = mLeftMenu.getMeasuredHeight();
Log.d("Slide","getWidth:"+mLeftMenu.getWidth()+" mLeftMenu.getHeight():"+mLeftMenu.getHeight());
Log.d("Slide","getMeasuredWidth:"+mLeftMenu.getMeasuredWidth()+" getMeasuredHeight:"+mLeftMenu.getMeasuredHeight());
// l t r b 左上右下 左上一点,右下一点,两点确定了一个矩形的大小
mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
int mainViewWidth = mMainPage.getMeasuredWidth();
int mainViewHeight = mMainPage.getMeasuredHeight();
mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
downX = (int)(event.getX()+0.5f);
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int)(event.getX()+0.5f);
// 用减法,比如按下是0,最终移动到了20,那么屏幕向左边移动20个单位,左侧菜单就可显示出来了
int distanceX = downX - moveX;
int scrollX = getScrollX(); // 注意getScaleX()不要写成getScaleX()
// 解决越界问题
if(scrollX+distanceX < (-mLeftMenu.getMeasuredWidth())){ // 左侧越界临界点
scrollTo(-mLeftMenu.getMeasuredWidth(),0);
}else if(scrollX+distanceX>0){ // 右侧越界临界点
scrollTo(0,0);
}else{ // 在两个临界点之间的可移动范围
scrollBy(distanceX,0);
}
downX = moveX;
break;
case MotionEvent.ACTION_UP:
break;
}
//关键一步,返回true,代表消费当前的偷吃事件
return true;
}
}
六.2、判断松手后应该停留在哪一个界面
做的事情很简单,其实也就是在 case MotionEvent.ACTION_UP: 里面添加几行代码
case MotionEvent.ACTION_UP:
int upScrollX = getScrollX();
if(upScrollX<-(mLeftMenu.getMeasuredWidth()/2)){
scrollTo(-mLeftMenu.getMeasuredWidth(),0);
}else{
scrollTo(0,0);
}
break;
六.3、使用Scoller弹性滑动,让滑动产生过渡效果
自定义View做动画有很多做法,Scroller是其中一种,也是我们这次要采用的做法。
Scoller弹性滑动是使用过程:
1、实例化Scroller
实例化一个Scroller
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
scroller = new Scroller(getContext()); // 实例化一个Scroll,需要context
}
2、调用startScroll(startX, startY, dx, dy, durationTime);模拟数据变化,接着调用invalidate(); 触发computeScroll()
注意点:startScroll 只是模拟数据的变化,想要看到效果还需要调用invalidate重新刷新UI,其实就是调用onDraw
注意点:invalidate();经过辗转会去computeScroll();
invalidate(); ---> draw()-->onDraw()--> computeScroll();
3、在computeScroll()里面复写真正让让模拟数据生效的代码,调用invalidate()
注意点:
scroller.computeScrollOffset()
computeScrollOffset()为true代表模拟数据还没有完成
注意点:scroller.getCurrX()当前时刻正模拟到的数据
代码如下(其实这几乎是模板代码):
@Override
public void computeScroll() { // 如果数据模拟没有完成,那么继续更新
//super.computeScroll();
// computeScrollOffset
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),0);
invalidate(); // 注意这里还调用了invalidate,这样才会产生效果
}
}
.
.
上面几点已经说完,具体看看在代码中怎么使用吧
computeScroll();和computeScrollOffset()的结合使用
case MotionEvent.ACTION_UP:
int upScrollX = getScrollX();
choosePage(upScrollX<-(mLeftMenu.getMeasuredWidth()/2));
break;
}
//关键一步,返回true,代表消费当前的偷吃事件
return true;
}
private void choosePage(boolean isMainPage){
if(isMainPage){
startScrollNow(-mLeftMenu.getMeasuredWidth());
//scrollTo(0,0); 可以实现但是无动画过渡
}else{
startScrollNow(0);
//scrollTo(-mLeftMenu.getMeasuredWidth(),0); 可以实现但是无动画过渡
}
}
private void startScrollNow(int endX){
int startX = getScrollX(); // 起始X
int startY = 0; // 起始Y
int dx = endX - startX; // X方向的增量值,可以理解为距离
int dy = 0; // Y方向的增量值,可以理解为距离
int time = Math.abs(dx) * 10;
int durationTime = (time>600)?600:time;
//startScroll(int startX, int startY, int dx, int dy, int duration)
// 注意: startScroll 只是模拟数据的变化,想要看到效果还需要调用invalidate重新刷新UI,其实就是调用onDraw
scroller.startScroll(startX, startY, dx, dy, durationTime);
invalidate(); // 关键代码,invalidate和computeScroll才会有动画效果,scroller.startScroll只是模拟数据
}
@Override
public void computeScroll() { // 如果数据模拟没有完成,那么继续更新
//super.computeScroll();
// computeScrollOffset
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),0);
invalidate(); // 注意这里还调用了invalidate,这样才会产生效果
}
}
六、4、添加是 否处于主页的方法 和 展示那个页面的方法
其实也就是添加了isAtMainPage、showMainPage()和showLeftPage()这三个方法
public class SlideMenu extends ViewGroup{
private static final int MAIN_PAGE = 0;
private static final int LEFT_PAGE = 1;
private View mLeftMenu;
private View mMainPage;
private int downX;
private Scroller scroller;
private int pageIndex;
public SlideMenu(Context context) {
super(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 当SlideView被xml引用加载之后完成,这个方法就会调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate(); // ???
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
scroller = new Scroller(getContext()); // 实例化一个Scroll,需要context
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// makeMeasureSpec 的时候,第一个参数是大小,第二个参数是模式
// 宽度我们使用左侧菜单的在xml里面的200dp,因为指定大小所以模式是MeasureSpec.EXACTLY
// 怎么得到一个View在xml布局文件里面宽,利用view.getLayoutParams().width,高类似
int leftViewMeasureSpecWidth = MeasureSpec.
makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
//左侧菜单的的高度我们希望填充父窗体,而当前onMeasure里面的heightMeasureSpec根据我们的布局显然就是填充父窗体
// 所以一直接用父亲传过来的这个32位参数就好
mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
// 至于主页页面,我们的希望他宽高都是填充父窗体,所以直接用onMeasure里面传过来的参数就好啦
mMainPage.measure(widthMeasureSpec, heightMeasureSpec);
// onMeasure一开始什么都不管就应该复写setMeasuredDimension,不然报错。
// 除非我们的自定义控件是宽高都是填充父窗体,那么我们就留着下面这句super的代码就可以
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftViewWidth = mLeftMenu.getMeasuredWidth();
int leftViewHeight = mLeftMenu.getMeasuredHeight();
Log.d("Slide", "getWidth:" + mLeftMenu.getWidth() + " mLeftMenu.getHeight():" + mLeftMenu.getHeight());
Log.d("Slide", "getMeasuredWidth:" + mLeftMenu.getMeasuredWidth() + " getMeasuredHeight:" + mLeftMenu.getMeasuredHeight());
// l t r b 左上右下 左上一点,右下一点,两点确定了一个矩形的大小
mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
int mainViewWidth = mMainPage.getMeasuredWidth();
int mainViewHeight = mMainPage.getMeasuredHeight();
mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
downX = (int)(event.getX()+0.5f);
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int)(event.getX()+0.5f);
// 用减法,比如按下是0,最终移动到了20,那么屏幕向左边移动20个单位,左侧菜单就可显示出来了
int distanceX = downX - moveX;
int scrollX = getScrollX(); // 注意getScaleX()不要写成getScaleX()
// 解决越界问题
if(scrollX+distanceX < (-mLeftMenu.getMeasuredWidth())){ // 左侧越界临界点
scrollTo(-mLeftMenu.getMeasuredWidth(),0);
}else if(scrollX+distanceX>0){ // 右侧越界临界点
scrollTo(0,0);
}else{ // 在两个临界点之间的可移动范围
scrollBy(distanceX,0);
}
downX = moveX;
break;
case MotionEvent.ACTION_UP:
int upScrollX = getScrollX();
choosePage(upScrollX<-(mLeftMenu.getMeasuredWidth()/2));
break;
}
//关键一步,返回true,代表消费当前的touch事件
return true;
}
private void choosePage(boolean isMainPage){
if(isMainPage){
pageIndex = MAIN_PAGE;
startScrollNow(-mLeftMenu.getMeasuredWidth());
//scrollTo(0,0); 可以实现但是无动画过渡
}else{
pageIndex = LEFT_PAGE;
startScrollNow(0);
//scrollTo(-mLeftMenu.getMeasuredWidth(),0); 可以实现但是无动画过渡
}
}
private void startScrollNow(int endX){
int startX = getScrollX(); // 起始X
int startY = 0; // 起始Y
int dx = endX - startX; // X方向的增量值,可以理解为距离
int dy = 0; // Y方向的增量值,可以理解为距离
int time = Math.abs(dx) * 10;
int durationTime = (time>600)?600:time;
//startScroll(int startX, int startY, int dx, int dy, int duration)
// 注意: startScroll 只是模拟数据的变化,想要看到效果还需要调用invalidate重新刷新UI,其实就是调用onDraw
scroller.startScroll(startX, startY, dx, dy, durationTime);
invalidate(); // 关键代码,invalidate和computeScroll才会有动画效果,scroller.startScroll只是模拟数据
}
@Override
public void computeScroll() { // 如果数据模拟没有完成,那么继续更新
//super.computeScroll();
// computeScrollOffset
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),0);
invalidate(); // 注意这里还调用了invalidate,这样才会产生效果
}
}
public boolean isAtMainPage(){
return (pageIndex == MAIN_PAGE)?true:false;
}
public void showMainPage(){
choosePage(true);
}
public void showLeftPage(){
choosePage(false);
}
}
六、5、我们发现当我们滑动左侧菜单的时候,无法拉动菜单。
这就涉及到一个View的传递机制了。
关于View的传递机制,可以参考文章 ——————————
为什么拉动左侧的菜单无法拖动,这肯定是属于SlideMenu的左侧菜单在拉动的时候,SlideMenu的onTouchEvent没有被执行,为了确保我们的SlideMenu不管是在左侧菜单还是主页菜单的都能够顺利拖动,我们就在SlideMenu里面复写 onInterceptTouchEvent 方法,然后判断一下,如果是横向滑动,就拦截下来,自己就消费掉这个touch
其实也就只是添加这么一小段代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
interDownX = (int)(ev.getX()+0.5f);
interDownY = (int)(ev.getY()+0.5f);
break;
case MotionEvent.ACTION_MOVE:
interMoveX = (int)(ev.getX()+0.5f);
interMoveY = (int)(ev.getY()+0.5f);
int diatanceInterX = Math.abs(interMoveX - interDownX);
int distanceInterY = Math.abs(interMoveY - interDownY);
if(diatanceInterX > distanceInterY){ // 代表是水平滑动,(水平滑动的距离比垂直滑动的距离大)
return true; // 拦截之后就肯定执行onTouchEvent
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev); // 如果不是水平滑动就不拦截
}
好啦,该做的差不多都做啦。
最后附上所有代码:
SlideMenu完整代码
public class SlideMenu extends ViewGroup{
private static final int MAIN_PAGE = 0;
private static final int LEFT_PAGE = 1;
private View mLeftMenu;
private View mMainPage;
private int downX;
private Scroller scroller;
private int pageIndex;
private int interDownX;
private int interDownY;
private int interMoveX;
private int interMoveY;
public SlideMenu(Context context) {
super(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 当SlideView被xml引用加载之后完成,这个方法就会调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate(); // ???
// getChildAt 作用 Returns the view at the specified position in the group.
// 精确地返回在ViewGroup里面的View的位置 所以我们的include顺序很重要
mLeftMenu = getChildAt(0); // 左侧菜单
mMainPage = getChildAt(1);
scroller = new Scroller(getContext()); // 实例化一个Scroll,需要context
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// makeMeasureSpec 的时候,第一个参数是大小,第二个参数是模式
// 宽度我们使用左侧菜单的在xml里面的200dp,因为指定大小所以模式是MeasureSpec.EXACTLY
// 怎么得到一个View在xml布局文件里面宽,利用view.getLayoutParams().width,高类似
int leftViewMeasureSpecWidth = MeasureSpec.
makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
//左侧菜单的的高度我们希望填充父窗体,而当前onMeasure里面的heightMeasureSpec根据我们的布局显然就是填充父窗体
// 所以一直接用父亲传过来的这个32位参数就好
mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
// 至于主页页面,我们的希望他宽高都是填充父窗体,所以直接用onMeasure里面传过来的参数就好啦
mMainPage.measure(widthMeasureSpec, heightMeasureSpec);
// onMeasure一开始什么都不管就应该复写setMeasuredDimension,不然报错。
// 除非我们的自定义控件是宽高都是填充父窗体,那么我们就留着下面这句super的代码就可以
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftViewWidth = mLeftMenu.getMeasuredWidth();
int leftViewHeight = mLeftMenu.getMeasuredHeight();
Log.d("Slide", "getWidth:" + mLeftMenu.getWidth() + " mLeftMenu.getHeight():" + mLeftMenu.getHeight());
Log.d("Slide", "getMeasuredWidth:" + mLeftMenu.getMeasuredWidth() + " getMeasuredHeight:" + mLeftMenu.getMeasuredHeight());
// l t r b 左上右下 左上一点,右下一点,两点确定了一个矩形的大小
mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
int mainViewWidth = mMainPage.getMeasuredWidth();
int mainViewHeight = mMainPage.getMeasuredHeight();
mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
interDownX = (int)(ev.getX()+0.5f);
interDownY = (int)(ev.getY()+0.5f);
break;
case MotionEvent.ACTION_MOVE:
interMoveX = (int)(ev.getX()+0.5f);
interMoveY = (int)(ev.getY()+0.5f);
int diatanceInterX = Math.abs(interMoveX - interDownX);
int distanceInterY = Math.abs(interMoveY - interDownY);
if(diatanceInterX > distanceInterY){ // 代表是水平滑动,(水平滑动的距离比垂直滑动的距离大)
return true; // 拦截之后就肯定执行onTouchEvent
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev); // 如果不是水平滑动就不拦截
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
downX = (int)(event.getX()+0.5f);
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int)(event.getX()+0.5f);
// 用减法,比如按下是0,最终移动到了20,那么屏幕向左边移动20个单位,左侧菜单就可显示出来了
int distanceX = downX - moveX;
int scrollX = getScrollX(); // 注意getScaleX()不要写成getScaleX()
// 解决越界问题
if(scrollX+distanceX < (-mLeftMenu.getMeasuredWidth())){ // 左侧越界临界点
scrollTo(-mLeftMenu.getMeasuredWidth(),0);
}else if(scrollX+distanceX>0){ // 右侧越界临界点
scrollTo(0,0);
}else{ // 在两个临界点之间的可移动范围
scrollBy(distanceX,0);
}
downX = moveX;
break;
case MotionEvent.ACTION_UP:
int upScrollX = getScrollX();
choosePage(upScrollX<-(mLeftMenu.getMeasuredWidth()/2));
break;
}
//关键一步,返回true,代表消费当前的touch事件
return true;
}
private void choosePage(boolean isMainPage){
if(isMainPage){
pageIndex = MAIN_PAGE;
startScrollNow(-mLeftMenu.getMeasuredWidth());
//scrollTo(0,0); 可以实现但是无动画过渡
}else{
pageIndex = LEFT_PAGE;
startScrollNow(0);
//scrollTo(-mLeftMenu.getMeasuredWidth(),0); 可以实现但是无动画过渡
}
}
private void startScrollNow(int endX){
int startX = getScrollX(); // 起始X
int startY = 0; // 起始Y
int dx = endX - startX; // X方向的增量值,可以理解为距离
int dy = 0; // Y方向的增量值,可以理解为距离
int time = Math.abs(dx) * 10;
int durationTime = (time>600)?600:time;
//startScroll(int startX, int startY, int dx, int dy, int duration)
// 注意: startScroll 只是模拟数据的变化,想要看到效果还需要调用invalidate重新刷新UI,其实就是调用onDraw
scroller.startScroll(startX, startY, dx, dy, durationTime);
invalidate(); // 关键代码,invalidate和computeScroll才会有动画效果,scroller.startScroll只是模拟数据
}
@Override
public void computeScroll() { // 如果数据模拟没有完成,那么继续更新
//super.computeScroll();
// computeScrollOffset
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),0);
invalidate(); // 注意这里还调用了invalidate,这样才会产生效果
}
}
public boolean isAtMainPage(){
return (pageIndex == MAIN_PAGE)?true:false;
}
public void showMainPage(){
choosePage(true);
}
public void showLeftPage(){
choosePage(false);
}
}
.
.
MainActivity
public class MainActivity extends Activity {
private SlideMenu slideMenu;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
slideMenu = (SlideMenu) findViewById(R.id.mSmenu);
findViewById(R.id.mIvBack).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(slideMenu.isAtMainPage()){
slideMenu.showLeftPage();
}else{
slideMenu.showMainPage();
}
}
});
findViewById(R.id.mTvBd).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,"本地",Toast.LENGTH_SHORT).show();
if(slideMenu.isAtMainPage()){
slideMenu.showLeftPage();
}else{
slideMenu.showMainPage();
}
}
});
}
}
组合控件仿qq侧滑菜单至此结束。
在组合控件1—— 设置框一文中,我们将进行组合控件的demo编写。
本篇完。