上篇給大家介紹QQ5.0側滑菜單的視頻課程,對于側滑的時的動畫效果的實現有了新的認識,似乎打通了任督二脈,目前可以實現任意效果的側滑菜單了,感謝鴻洋大大??!
用的是HorizontalScrollView來實現的側滑菜單功能,HorizontalScrollView的好處是為我們解決了滑動功能,處理了滑動沖突問題,讓我們使用起來非常方便,但是滑動和沖突處理都是android中的難點,是我們應該掌握的知識點,掌握了這些,我們可以不依賴于系統的API,隨心所欲打造我們想要的效果,因此這篇文章我將直接自定義ViewGroup來實現側滑菜單功能
首先我們先來看一看效果圖,第一個效果圖是一個最普通的側滑菜單,我們一會兒會先做出這種側滑菜單,然后再在此基礎上實現另外兩個效果
第一種
第二種
第三種
實現第一種側滑菜單,繼承自ViewGroup
繼承自ViewGroup需要我們自己來測量,布局,實現滑動的效果,處理滑動沖突,這些都是一些新手無從下手的知識點,希望看了這篇文章后可以對大家有一個幫助
自定義ViewGroup的一般思路是重寫onMeasure方法,在onMeasure方法中調用measureChild來測量子View,然后調用setMeasuredDimension來測量自己的大小。然后重寫onLayout方法,在onLayout中調用子View的layout方法來確定子View的位置,下面我們先來做好這兩件工作
初始時候我們的Content應該是顯示在屏幕中的,而Menu應該是顯示在屏幕外的。當Menu打開時,應該是這種樣子的
mMenuRightPadding是Menu距屏幕右側的一個距離,因為我們Menu打開后,Content還是會留一部分,而不是完全隱藏的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
public class MySlidingMenu extends ViewGroup { public MySlidingMenu(Context context) { this (context, null , 0 ); } public MySlidingMenu(Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); DisplayMetrics metrics = new DisplayMetrics(); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getMetrics(metrics); //獲取屏幕的寬和高 mScreenWidth = metrics.widthPixels; mScreenHeight = metrics.heightPixels; //設置Menu距離屏幕右側的距離,convertToDp是將代碼中的100轉換成100dp mMenuRightPadding = convertToDp(context, 100 ); } @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { //拿到Menu,Menu是第0個孩子 mMenu = (ViewGroup) getChildAt( 0 ); //拿到Content,Content是第1個孩子 mContent = (ViewGroup) getChildAt( 1 ); //設置Menu的寬為屏幕的寬度減去Menu距離屏幕右側的距離 mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding; //設置Content的寬為屏幕的寬度 mContentWidth = mContent.getLayoutParams().width = mScreenWidth; //測量Menu measureChild(mMenu,widthMeasureSpec,heightMeasureSpec); //測量Content measureChild(mContent, widthMeasureSpec, heightMeasureSpec); //測量自己,自己的寬度為Menu寬度加上Content寬度,高度為屏幕高度 setMeasuredDimension(mMenuWidth + mContentWidth, mScreenHeight); } @Override protected void onLayout( boolean changed, int l, int t, int r, int b) { //擺放Menu的位置,根據上面圖可以確定上下左右的坐標 mMenu.layout(-mMenuWidth, 0 , 0 , mScreenHeight); //擺放Content的位置 mContent.layout( 0 , 0 , mScreenWidth, mScreenHeight); } /** * 將傳進來的數轉化為dp */ private int convertToDp(Context context , int num){ return ( int ) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,num,context.getResources().getDisplayMetrics()); } } |
目前我們的側滑菜單中的兩個子View的位置應該是這個樣子
接下來我們編寫xml布局文件
left_menu.xml 左側菜單的布局文件,是一個ListView
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?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" > <ListView android:id= "@+id/menu_listview" android:layout_width= "wrap_content" android:divider= "@null" android:dividerHeight= "0dp" android:scrollbars= "none" android:layout_height= "wrap_content" > </ListView> </RelativeLayout> |
其中ListView的Item布局為left_menu_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:orientation= "horizontal" android:layout_width= "match_parent" android:gravity= "center_vertical" android:layout_height= "match_parent" > <ImageView android:id= "@+id/menu_imageview" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:src= "@drawable/menu_1" android:padding= "20dp" /> <TextView android:id= "@+id/menu_textview" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:text= "菜單1" android:textColor= "#000000" android:textSize= "20sp" /> </LinearLayout> |
我們再來編寫內容區域的布局文件 content.xml 其中有一個header,header中有一個ImageView,這個ImageView是menu的開關,我們點擊他的時候可以自動開關menu,然后header下面也是一個listview
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:orientation= "vertical" android:layout_width= "match_parent" android:layout_height= "match_parent" > <LinearLayout android:layout_width= "match_parent" android:layout_height= "65dp" android:background= "#000000" android:gravity= "center_vertical" android:orientation= "horizontal" > <ImageView android:id= "@+id/menu_toggle" android:layout_width= "40dp" android:layout_height= "40dp" android:src= "@drawable/toggle" android:paddingLeft= "10dp" /> </LinearLayout> <ListView android:id= "@+id/content_listview" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:dividerHeight= "0dp" android:divider= "@null" android:scrollbars= "none" /> </LinearLayout> |
content的item的布局文件為 content_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:orientation= "horizontal" android:layout_width= "match_parent" android:gravity= "center_vertical" android:background= "#ffffff" android:layout_height= "match_parent" > <ImageView android:id= "@+id/content_imageview" android:layout_width= "80dp" android:layout_height= "80dp" android:src= "@drawable/content_1" android:layout_margin= "20dp" /> <TextView android:id= "@+id/content_textview" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:text= "Content - 1" android:textColor= "#000000" android:textSize= "20sp" /> </LinearLayout> |
在activity_main.xml中,我們將menu和content添加到我們的slidingMenu中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:background= "#aaaaaa" > <com.example.user.slidingmenu.MySlidingMenu android:id= "@+id/slidingmenu" android:layout_width= "wrap_content" android:layout_height= "match_parent" > <include android:id= "@+id/menu" layout= "@layout/left_menu" /> <include android:id= "@+id/content" layout= "@layout/content" /> </com.example.user.slidingmenu.MySlidingMenu> </RelativeLayout> |
現在應該是這種效果
左側菜單是隱藏在屏幕左側外部的,但是現在還不能滑動,如果想要實現滑動功能,我們可以使用View的scrollTo和scrollBy方法,這兩個方法的區別是scrollTo是直接將view移動到指定的位置,scrollBy是相對于當前的位置移動一個偏移量,所以我們應該重寫onTouchEvent方法,用來計算出當前手指的一個偏移量,然后使用scrollBy方法一點一點的移動,就形成了一個可以跟隨手指移動的view的動畫效果了
在寫代碼之前,我們先掃清一下障礙,我們先來弄清楚這些坐標是怎么回事
好了,把這些坐標弄清楚后,我們就簡單多了,下面直接看onTouchEvent方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action){ case MotionEvent.ACTION_DOWN: mLastX = ( int ) event.getX(); mLastY = ( int ) event.getY(); break ; case MotionEvent.ACTION_MOVE: int currentX = ( int ) event.getX(); int currentY = ( int ) event.getY(); //拿到x方向的偏移量 int dx = currentX - mLastX; if (dx < 0 ){ //向左滑動 //邊界控制,如果Menu已經完全顯示,再滑動的話 //Menu左側就會出現白邊了,進行邊界控制 if (getScrollX() + Math.abs(dx) >= 0 ) { //直接移動到(0,0)位置,不會出現白邊 scrollTo( 0 , 0 ); } else { //Menu沒有完全顯示呢 //其實這里dx還是-dx,大家不用刻意去記 //大家可以先使用dx,然后運行一下,發現 //移動的方向是相反的,那么果斷這里加個負號就可以了 scrollBy(-dx, 0 ); } } else { //向右滑動 //邊界控制,如果Content已經完全顯示,再滑動的話 //Content右側就會出現白邊了,進行邊界控制 if (getScrollX() - dx <= -mMenuWidth) { //直接移動到(-mMenuWidth,0)位置,不會出現白邊 scrollTo(-mMenuWidth, 0 ); } else { //Content沒有完全顯示呢 //根據手指移動 scrollBy(-dx, 0 ); } } mLastX = currentX; mLastY = currentY; break ; } return true ; } |
現在我們的SlidingMenu依然是不能夠水平滑動的,但是listview可以豎直滑動,原因是我們的SlidingMenu默認是不攔截事件的,那么事件會傳遞給他的子View去執行,也就是說傳遞給了Content的ListView去執行了,所以listview是可以滑動的,為了簡單,我們先重寫onInterceptTouchEvent方法,我們返回true,讓SlidingMenu攔截事件,我們的SlidingMenu就能夠滑動了,但是ListView是不能滑動的,等下我們會進行滑動沖突的處理,現在先實現SlidingMenu的功能
1
2
3
4
|
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true ; } |
好了,現在我們可以自由的滑動我們的SlidingMenu了,并且進行了很好的邊界控制,現在我們再添加個功能,就是當Menu打開大于二分之一時,松開手指,Menu自動打開。當Menu打開小于二分之一時,松開手指,Menu自動關閉。自動滑動的功能我們要借助Scroller來實現
我們在構造方法中初始化一個Scroller
1
2
3
4
5
6
|
public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); ... mScroller = new Scroller(context); ... } |
然后重寫computeScroll方法,這個方法是保證Scroller自動滑動的必須方法,這是一個模板方法,到哪里都這么些就好了
1
2
3
4
5
6
7
|
@Override public void computeScroll() { if (mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } |
接著我們在onTouchEvent的ACTION_UP中進行判斷,判斷當前menu打開了多少
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
case MotionEvent.ACTION_UP: if (getScrollX() < -mMenuWidth / 2 ){ //打開Menu //調用startScroll方法,第一個參數是起始X坐標,第二個參數 //是起始Y坐標,第三個參數是X方向偏移量,第四個參數是Y方向偏移量 mScroller.startScroll(getScrollX(), 0 , -mMenuWidth - getScrollX(), 0 , 300 ); //設置一個已經打開的標識,當實現點擊開關自動打開關閉功能時會用到 isOpen = true ; //一定不要忘了調用這個方法重繪,否則沒有動畫效果 invalidate(); } else { //關閉Menu //同上 mScroller.startScroll(getScrollX(), 0 , -getScrollX(), 0 , 300 ); isOpen = false ; invalidate(); } break ; |
關于startScroll中的startX和startY好判斷,那么dx和dy怎么計算呢?其實也非常簡單,比如我們startX坐標為30,我們想移動到-100,那么startX+dx = -100 –> dx = -100 - startX –> dx = -130
好了現在我們就可以實現松開手指后自動滑動的動畫效果了
現在我們還需要點擊content中左上角的一個三角,如果當前menu沒有打開,則自動打開,如果已經打開,則自動關閉的功能,自動滑動的效果我們要借助Scroller.startScroll方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/** * 點擊開關,開閉Menu,如果當前menu已經打開,則關閉,如果當前menu已經關閉,則打開 */ public void toggleMenu(){ if (isOpen){ closeMenu(); } else { openMenu(); } } /** * 關閉menu */ private void closeMenu() { //也是使用startScroll方法,dx和dy的計算方法一樣 mScroller.startScroll(getScrollX(), 0 ,-getScrollX(), 0 , 500 ); invalidate(); isOpen = false ; } /** * 打開menu */ private void openMenu() { mScroller.startScroll(getScrollX(), 0 ,-mMenuWidth-getScrollX(), 0 , 500 ); invalidate(); isOpen = true ; } |
然后我們可以在MainActivity中拿到我們content左上角三角形的imageview,然后給他設置一個點擊事件,調用我們的toggleMenu方法
1
2
3
4
5
6
|
mMenuToggle.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { mSlidingMenu.toggleMenu(); } }); |
處理滑動沖突
由于我們的menu和content是listview,listview是支持豎直滑動的,而我們的slidingMenu是支持水平滑動的,因此會出現滑動的沖突。剛才我們直接在onInterceptTouchEvent中返回了true,因此SlidingMenu就會攔截所有的事件,而ListView接收不到任何的事件,因此ListView不能滑動了,我們要解決這個滑動沖突很簡單,只需要判斷當前是水平滑動還是豎直滑動,如果是水平滑動的話則讓SlidingMenu攔截事件,如果是豎直滑動的話就不攔截事件,把事件交給子View的ListView去執行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false ; int x = ( int ) ev.getX(); int y = ( int ) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercept = false ; break ; case MotionEvent.ACTION_MOVE: int deltaX = ( int ) ev.getX() - mLastXIntercept; int deltaY = ( int ) ev.getY() - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)){ //橫向滑動 intercept = true ; } else { //縱向滑動 intercept = false ; } break ; case MotionEvent.ACTION_UP: intercept = false ; break ; } mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercept; } |
好了,現在我們的滑動沖突就解決了,我們既可以水平滑動SlidingMenu,又可以豎直滑動ListView,那么第一種SlidingMenu就已經實現了,我們再來看看另外兩種怎么去實現
實現第二種QQ V6.2.3風格的SlidingMenu
這種SlidingMenu是和QQ v6.2.3 的側滑菜單風格一致的,我們發現Menu和Content的滑動速度是有一個速度差的,實際上我們可以通過修改Menu的偏移量來達到這種效果
此時Menu的偏移量為mMenuWidth的2/3,當我們慢慢打開Menu的同時,修改Menu的偏移量,最終修改為0
這樣就達到了一種速度差的效果,我們只需要在onTouchEvent的ACTION_MOVE和computeScroll中添加一行如下代碼就可以
1
|
mMenu.setTranslationX( 2 *(mMenuWidth+getScrollX())/ 3 ); |
我們分析一下,在最開始,mMenuWidth+getScrollX=mMenuWidth,再乘以2/3,得到的就是mMenuWidth的2/3 , 當我們滑動至Menu完全打開時,mMenuWidth+getScrollX=0 , 這就達到了我們的效果
為什么要在computeScroll中也添加這一行代碼呢,因為當我們滑動過程中,如果我們手指離開屏幕,ACTION_MOVE肯定就不執行了,但是當我們手指離開屏幕后,會有一段自動打開或者關閉的動畫,那么這段動畫應該繼續去設置Menu的偏移量,因此我們在computeScroll中也要添加這一行代碼。
好了,效果我們已經實現了,只需要去設置Menu的偏移量就可以了,是不是非常簡單
實現第三種QQ V5.0風格的SlidingMenu
這個效果中Menu有一個偏移的效果,透明度的變化以及放大的效果。Content中有一個縮小的效果。
首先我們要有一個變量,用來記錄當前menu已經打開了多少百分比。
這里我們要注意,getScrollX得到的數值正好是負值,所以我們計算的時候要將getScrollX的值取絕對值再去計算,我們在onTouchEvent的MOVE中要計算這個值,同時在computeScroll方法中也要計算這個值,因為當我們手指抬起時,可能會執行一段自動打開或者關閉的動畫,那么我們在MOVE中的計算肯定停止了,但是在執行動畫的過程中,是Scroller在起作用,那么computeScroll就會執行直到動畫結束,因此我們要在computeScroll中同樣進行計算
1
|
scale = Math.abs(( float )getScrollX()) / ( float ) mMenuWidth; |
scale的值是[0,1]的,因此我們就可以根據這個值來對menu的偏移量進行設置。
我們可以通過設置View的setScaleX和setScaleY來對View進行放大縮小,當然這個縮放比例要根據我們的scale值來改變,首先我們的Menu有一個放大的效果,我們就指定為Menu從0.7放大到1.0,那么我們就可以這樣寫
1
2
|
mMenu.setScaleX( 0 .7f + 0 .3f*scale); mMenu.setScaleY( 0 .7f + 0 .3f*scale); |
透明度是從0到1的,所以我們直接用scale的值就可以了
1
|
mMenu.setAlpha(scale); |
我還給Menu設置了一個偏移量,這個偏移量大家可以自己計算,我是這樣計算的
1
|
mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/ 2 )*( 1 .0f-scale)); |
設置完Menu后,我們再來設置Content,Content的大小是從1.0縮小到0.7,因此我們這樣寫
1
2
3
|
mContent.setScaleX( 1 - 0 .3f*scale); mContent.setPivotX( 0 ); mContent.setScaleY( 1 .0f - 0 .3f * scale); |
其中mContent.setPivotX(0)是讓Content的縮放中心店的X軸坐標為0點
我們可以將這個變化的過程抽取為一個方法
1
2
3
4
5
6
7
8
9
10
|
private void slidingMode3(){ mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/ 2 )*( 1 .0f-scale)); mMenu.setScaleX( 0 .7f + 0 .3f*scale); mMenu.setScaleY( 0 .7f + 0 .3f*scale); mMenu.setAlpha(scale); mContent.setScaleX( 1 - 0 .3f*scale); mContent.setPivotX( 0 ); mContent.setScaleY( 1 .0f - 0 .3f * scale); } |
將這個方法添加到onTouchEvent的ACTION_MOVE和computeScroll中就可以了。
我們看到所有的滑動風格都是在基于第一種基礎上,修改Menu或者Content的translationX或者scaleX scaleY的值來決定的,因此我們可以打造各種各樣的SlidingMenu來。
以上所述是小編給大家介紹的Android自定義ViewGroup打造各種風格的SlidingMenu的相關知識,希望對大家有所幫助!