在自定義控件——初識自定義控件里面,我們已經對自定義控件進行描述和分類。其分類分別是
- 自制控件
- 組合控件
- 拓展控件
這篇博文里面,我們繼續進行自制控件。
我們想要繼續的是一個簡單的仿造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編寫。
本篇完。