自定义控件-侧边菜单SlidingMenu(滑动菜单)
1.布局
activity_main.xml
<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.example.slidemenu.MainActivity" > <com.example.slidemenu.SlideMenu android:id="@+id/sm" android:layout_width="match_parent" android:layout_height="match_parent" >
<!-- 引入菜单布局, 索引为: 0 -->
<include layout="@layout/slide_menu" />
<!-- 引入菜单布局, 索引为: 0 -->
<include layout="@layout/slide_content" /> </com.example.slidemenu.SlideMenu> </RelativeLayout>
这个布局文件用到了自定义的ViewGroup对象com.example.slidemenu.SlideMenu,
继承了ViewGroup类的控件时可以在其内部嵌套子控件。
slide_content.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:background="@drawable/top_bar_bg" android:gravity="center_vertical" > <ImageView android:id="@+id/iv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/main_back" /> <TextView android:layout_width="1dp" android:layout_height="match_parent" android:background="@drawable/top_bar_divider" android:layout_margin="5dp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="新闻" android:textSize="30sp" android:textColor="#ffffff" /> </LinearLayout> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="30sp" android:text="!!!!!!!!!!!" android:gravity="center" /> </LinearLayout>
slide_menu.xml
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="240dp" android:layout_height="match_parent" > <LinearLayout android:layout_width="240dp" android:layout_height="match_parent" android:background="@drawable/menu_bg" android:orientation="vertical" > <TextView android:text="新闻" android:drawableLeft="@drawable/tab_news" style="@style/SlideMenuStyle" /> <TextView android:text="订阅" android:drawableLeft="@drawable/tab_read" style="@style/SlideMenuStyle" /> <TextView android:text="本地" android:drawableLeft="@drawable/tab_local" style="@style/SlideMenuStyle" /> <TextView android:text="跟帖" android:drawableLeft="@drawable/tab_ties" style="@style/SlideMenuStyle" /> <TextView android:text="图片" android:drawableLeft="@drawable/tab_pics" style="@style/SlideMenuStyle" /> <TextView android:text="话题" android:drawableLeft="@drawable/tab_ugc" style="@style/SlideMenuStyle" /> <TextView android:text="投票" android:drawableLeft="@drawable/tab_vote" style="@style/SlideMenuStyle" /> <TextView android:text="聚合阅读" android:drawableLeft="@drawable/tab_focus" style="@style/SlideMenuStyle" /> </LinearLayout> </ScrollView>
/res/values/styles.xml
<resources> <style name="AppBaseTheme" parent="Theme.AppCompat.Light"> </style> <!-- Application theme. --> <style name="AppTheme" parent="AppBaseTheme"> </style> <!-- slideMenu style --> <style name="SlideMenuStyle"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> <item name="android:gravity">center_vertical</item> <item name="android:textSize">18sp</item> <item name="android:textColor">#ffffff</item> <item name="android:background">@drawable/selector_slidemenuitem_bg</item> <item name="android:padding">10dp</item> <item name="android:drawablePadding">20dp</item> <item name="android:onClick">click</item> <item name="android:clickable">true</item> </style> </resources>
res/drawable-hdpi/selector_slidemenuitem_bg.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <!-- @android:color/transparent为透明色 --> <item android:drawable="@android:color/transparent" android:state_pressed="true"></item> <item android:drawable="@color/slidemenu_press_bg" android:state_pressed="false"></item> </selector>
res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="slidemenu_press_bg">#3333cccc</color> </resources>
业务逻辑实现
自定义ViewGroup控件
Android事件处理机制
--------------------------------------------------
自定义类SlideMenu继承ViewGroup。该类作为自定义控件类。
SlideMenu.java
package com.example.slidemenu; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; public class SlideMenu extends ViewGroup { private int downX;// 按下时x轴的偏移量 private int menuMeasuredWidth; int currentState = STATE_MAIN_VIEW;// 当前屏幕显示的界面, 默认为: 主界面 static final int STATE_MENU_VIEW = 0;// 菜单界面 static final int STATE_MAIN_VIEW = 1;// 主界面 // 利用这个对象做出滑动的动画效果 Scroller scroller; public SlideMenu(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub scroller = new Scroller(context); } // 测量子节点的大小 /** * widthMeasureSpec 填充屏幕 heightMeasureSpec 填充屏幕 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 调用子节点的measure方法,该方法实际是调用了子节点的onMeasure方法,测量子节点的大小 // 测量菜单的宽和高. 宽: 240dip, 高: 填充屏幕 View menuView = getChildAt(0); menuView.measure(menuView.getLayoutParams().width, heightMeasureSpec); // 测量主界面的宽和高. 宽: 填充屏幕, 高: 填充屏幕 View mainView = getChildAt(1); mainView.measure(widthMeasureSpec, heightMeasureSpec); } // 分配空间 // 因为slideMenu占满了父元素,宽高与父元素相同 // l:0 // t:0 // r:slideMenu的宽,也就是父元素的宽 // b:slideMenu的高,也就是父元素的高 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub // 主界面的位置放置在屏幕左上角 View mainView = getChildAt(1); mainView.layout(l, t, r, b); // 把菜单的位置放置在屏幕的左侧 View menuView = getChildAt(0); menuMeasuredWidth = menuView.getMeasuredWidth(); menuView.layout(-menuView.getMeasuredWidth(), t, 0, b); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 只有在横着滑动时才可以拦截. switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: downX = (int) ev.getX(); break; case MotionEvent.ACTION_MOVE: int moveX = (int) ev.getX(); if (Math.abs(moveX - downX) > 10) { return true; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int moveX = (int) event.getX(); int offsetX = downX - moveX; // 判断一下用户手指滑动是否越界 // 判断给定当前的增量移动后, 是否能够超出边界. int scrollX = getScrollX(); if (scrollX + offsetX < -menuMeasuredWidth) { // 当前超出了左边界, 应该设置为在菜单的左边界位置上. scrollTo(-menuMeasuredWidth, 0); } else if (scrollX + offsetX > 0) { // 当前超出了右边界, 应该设置为0 scrollTo(0, 0); } else { scrollBy(offsetX, 0); } downX = moveX; break; case MotionEvent.ACTION_UP: // 获取菜单宽度的一半 int center = -menuMeasuredWidth / 2; scrollX = getScrollX();// 当前屏幕左上角的值 if (scrollX < center) { currentState = STATE_MENU_VIEW; refreshViewState(); } else { currentState = STATE_MAIN_VIEW; refreshViewState(); } break; } return true; } // 松手时根据滑动进度,让mScrollX复位 /** * 根据currentScreen变量来切换屏幕显示 */ private void refreshViewState() { // TODO Auto-generated method stub int startX = getScrollX();// 开始的位置 int dx = 0;// 增量值 = 目的地位置 - 开始的位置; if (currentState == STATE_MAIN_VIEW) { dx = 0 - startX; } else if (currentState == STATE_MENU_VIEW) { dx = -menuMeasuredWidth - startX; } int duration = Math.abs(dx) * 10; if (duration > 1600) { duration = 1600; } // arg0:滑动的开始坐标 // arg1:滑动的距离 scroller.startScroll(startX, 0, dx, 0, duration); // 只要调用invalidate,那么computeScroll方法就会调用 // 刷新当前控件, 会引起onDraw方法的调用. invalidate();// -> drawChild -> view.draw -> view.computeScroll } @Override public void computeScroll() { // scroller不会去改变mScrollX的值,它只是帮你计算出你应该把mScrollX的值变为多少 // 判断动画是否结束,如果时间到了,那么结束了,返回false,如果时间未到,返回true if (scroller.computeScrollOffset()) { // 当前mScrollX应该变为多少,这个返回值是startX+应该位移的距离 /* * scroller .startScroll(0 , 0 , 1000, 0,100); * * 当时间过去了1毫秒时,屏幕应该移动10像素 当时间过去了10毫秒时,屏幕应该移动100像素 例如: * 3点10分50000毫秒,调用那个了scroller.startScroll(0 , 0 , 1000, 0,100); * 3点10分50010毫秒,调用那个了scroller.getcurrx(),返回100 * 3点10分50050毫秒,调用那个了scroller.getcurrx(),返回500 */ int currX = scroller.getCurrX(); scrollTo(currX, 0); invalidate();// 在触发当前方法, 相当于递归. } } /** * 是否显示菜单 * * @return true 显示菜单, false 不显示 */ public boolean isShowMenu() { return currentState == STATE_MENU_VIEW; } /** * 隐藏菜单 */ public void hideMenuView() { currentState = STATE_MAIN_VIEW; refreshViewState(); } /** * 显示菜单 */ public void showMenuView() { currentState = STATE_MENU_VIEW; refreshViewState(); } }
MainActivity.xml
package com.example.slidemenu; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); final SlideMenu sm = (SlideMenu) findViewById(R.id.sm); ImageView iv = (ImageView) findViewById(R.id.iv); iv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 判断当前状态 if (sm.isShowMenu()) { // 是菜单界面, 切换到主界面 sm.hideMenuView(); } else { // 是主界面, 应该把菜单显示出来 sm.showMenuView(); } } }); } public void click(View v) { Toast.makeText(this, ((TextView) v).getText(), 0).show(); } }