之前我向大家介绍了史上最简单的滑动菜单的实现方式,相信大家都还记得。如果忘记了其中的实现原理或者还没看过的朋友,请先去看一遍之前的文章 Android滑动菜单特效实现,仿人人客户端侧滑效果,史上最简单的侧滑实现 ,因为我们今天要实现的滑动菜单框架也是基于同样的原理的。
之前的文章中在最后也提到了,如果是你的应用程序中有很多个Activity都需要加入滑动菜单的功能,那么每个Activity都要写上百行的代码才能实现效果,再简单的滑动菜单实现方案也没用。因此我们今天要实现一个滑动菜单的框架,然后在任何Activity中都可以一分钟引入滑动菜单功能。
首先还是讲一下实现原理。说是滑动菜单的框架,其实说白了也很简单,就是我们自定义一个布局,在这个自定义布局中实现好滑动菜单的功能,然后只要在Activity的布局文件里面引入我们自定义的布局,这个Activity就拥有了滑动菜单的功能了。原理讲完了,是不是很简单?下面我们来动手实现吧。
在Eclipse中新建一个Android项目,项目名就叫做RenRenSlidingLayout。
新建一个类,名叫SlidingLayout,这个类是继承自LinearLayout的,并且实现了OnTouchListener接口,具体代码如下:
- public class SlidingLayout extends LinearLayout implements OnTouchListener {
- /**
- * 滚动显示和隐藏左侧布局时,手指滑动需要达到的速度。
- */
- public static final int SNAP_VELOCITY = 200;
- /**
- * 屏幕宽度值。
- */
- private int screenWidth;
- /**
- * 左侧布局最多可以滑动到的左边缘。值由左侧布局的宽度来定,marginLeft到达此值之后,不能再减少。
- */
- private int leftEdge;
- /**
- * 左侧布局最多可以滑动到的右边缘。值恒为0,即marginLeft到达0之后,不能增加。
- */
- private int rightEdge = 0;
- /**
- * 左侧布局完全显示时,留给右侧布局的宽度值。
- */
- private int leftLayoutPadding = 80;
- /**
- * 记录手指按下时的横坐标。
- */
- private float xDown;
- /**
- * 记录手指移动时的横坐标。
- */
- private float xMove;
- /**
- * 记录手机抬起时的横坐标。
- */
- private float xUp;
- /**
- * 左侧布局当前是显示还是隐藏。只有完全显示或隐藏时才会更改此值,滑动过程中此值无效。
- */
- private boolean isLeftLayoutVisible;
- /**
- * 左侧布局对象。
- */
- private View leftLayout;
- /**
- * 右侧布局对象。
- */
- private View rightLayout;
- /**
- * 用于监听侧滑事件的View。
- */
- private View mBindView;
- /**
- * 左侧布局的参数,通过此参数来重新确定左侧布局的宽度,以及更改leftMargin的值。
- */
- private MarginLayoutParams leftLayoutParams;
- /**
- * 右侧布局的参数,通过此参数来重新确定右侧布局的宽度。
- */
- private MarginLayoutParams rightLayoutParams;
- /**
- * 用于计算手指滑动的速度。
- */
- private VelocityTracker mVelocityTracker;
- /**
- * 重写SlidingLayout的构造函数,其中获取了屏幕的宽度。
- *
- * @param context
- * @param attrs
- */
- public SlidingLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- screenWidth = wm.getDefaultDisplay().getWidth();
- }
- /**
- * 绑定监听侧滑事件的View,即在绑定的View进行滑动才可以显示和隐藏左侧布局。
- *
- * @param bindView
- * 需要绑定的View对象。
- */
- public void setScrollEvent(View bindView) {
- mBindView = bindView;
- mBindView.setOnTouchListener(this);
- }
- /**
- * 将屏幕滚动到左侧布局界面,滚动速度设定为30.
- */
- public void scrollToLeftLayout() {
- new ScrollTask().execute(30);
- }
- /**
- * 将屏幕滚动到右侧布局界面,滚动速度设定为-30.
- */
- public void scrollToRightLayout() {
- new ScrollTask().execute(-30);
- }
- /**
- * 左侧布局是否完全显示出来,或完全隐藏,滑动过程中此值无效。
- *
- * @return 左侧布局完全显示返回true,完全隐藏返回false。
- */
- public boolean isLeftLayoutVisible() {
- return isLeftLayoutVisible;
- }
- /**
- * 在onLayout中重新设定左侧布局和右侧布局的参数。
- */
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- if (changed) {
- // 获取左侧布局对象
- leftLayout = getChildAt(0);
- leftLayoutParams = (MarginLayoutParams) leftLayout.getLayoutParams();
- // 重置左侧布局对象的宽度为屏幕宽度减去leftLayoutPadding
- leftLayoutParams.width = screenWidth - leftLayoutPadding;
- // 设置最左边距为负的左侧布局的宽度
- leftEdge = -leftLayoutParams.width;
- leftLayoutParams.leftMargin = leftEdge;
- leftLayout.setLayoutParams(leftLayoutParams);
- // 获取右侧布局对象
- rightLayout = getChildAt(1);
- rightLayoutParams = (MarginLayoutParams) rightLayout.getLayoutParams();
- rightLayoutParams.width = screenWidth;
- rightLayout.setLayoutParams(rightLayoutParams);
- }
- }
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- createVelocityTracker(event);
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // 手指按下时,记录按下时的横坐标
- xDown = event.getRawX();
- break;
- case MotionEvent.ACTION_MOVE:
- // 手指移动时,对比按下时的横坐标,计算出移动的距离,来调整左侧布局的leftMargin值,从而显示和隐藏左侧布局
- xMove = event.getRawX();
- int distanceX = (int) (xMove - xDown);
- if (isLeftLayoutVisible) {
- leftLayoutParams.leftMargin = distanceX;
- } else {
- leftLayoutParams.leftMargin = leftEdge + distanceX;
- }
- if (leftLayoutParams.leftMargin < leftEdge) {
- leftLayoutParams.leftMargin = leftEdge;
- } else if (leftLayoutParams.leftMargin > rightEdge) {
- leftLayoutParams.leftMargin = rightEdge;
- }
- leftLayout.setLayoutParams(leftLayoutParams);
- break;
- case MotionEvent.ACTION_UP:
- // 手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局
- xUp = event.getRawX();
- if (wantToShowLeftLayout()) {
- if (shouldScrollToLeftLayout()) {
- scrollToLeftLayout();
- } else {
- scrollToRightLayout();
- }
- } else if (wantToShowRightLayout()) {
- if (shouldScrollToContent()) {
- scrollToRightLayout();
- } else {
- scrollToLeftLayout();
- }
- }
- recycleVelocityTracker();
- break;
- }
- return isBindBasicLayout();
- }
- /**
- * 判断当前手势的意图是不是想显示右侧布局。如果手指移动的距离是负数,且当前左侧布局是可见的,则认为当前手势是想要显示右侧布局。
- *
- * @return 当前手势想显示右侧布局返回true,否则返回false。
- */
- private boolean wantToShowRightLayout() {
- return xUp - xDown < 0 && isLeftLayoutVisible;
- }
- /**
- * 判断当前手势的意图是不是想显示左侧布局。如果手指移动的距离是正数,且当前左侧布局是不可见的,则认为当前手势是想要显示左侧布局。
- *
- * @return 当前手势想显示左侧布局返回true,否则返回false。
- */
- private boolean wantToShowLeftLayout() {
- return xUp - xDown > 0 && !isLeftLayoutVisible;
- }
- /**
- * 判断是否应该滚动将左侧布局展示出来。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY,
- * 就认为应该滚动将左侧布局展示出来。
- *
- * @return 如果应该滚动将左侧布局展示出来返回true,否则返回false。
- */
- private boolean shouldScrollToLeftLayout() {
- return xUp - xDown > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
- }
- /**
- * 判断是否应该滚动将右侧布局展示出来。如果手指移动距离加上leftLayoutPadding大于屏幕的1/2,
- * 或者手指移动速度大于SNAP_VELOCITY, 就认为应该滚动将右侧布局展示出来。
- *
- * @return 如果应该滚动将右侧布局展示出来返回true,否则返回false。
- */
- private boolean shouldScrollToContent() {
- return xDown - xUp + leftLayoutPadding > screenWidth / 2
- || getScrollVelocity() > SNAP_VELOCITY;
- }
- /**
- * 判断绑定滑动事件的View是不是一个基础layout,不支持自定义layout,只支持四种基本layout,
- * AbsoluteLayout已被弃用。
- *
- * @return 如果绑定滑动事件的View是LinearLayout,RelativeLayout,FrameLayout,
- * TableLayout之一就返回true,否则返回false。
- */
- private boolean isBindBasicLayout() {
- if (mBindView == null) {
- return false;
- }
- String viewName = mBindView.getClass().getName();
- return viewName.equals(LinearLayout.class.getName())
- || viewName.equals(RelativeLayout.class.getName())
- || viewName.equals(FrameLayout.class.getName())
- || viewName.equals(TableLayout.class.getName());
- }
- /**
- * 创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。
- *
- * @param event
- * 右侧布局监听控件的滑动事件
- */
- private void createVelocityTracker(MotionEvent event) {
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- }
- /**
- * 获取手指在右侧布局的监听View上的滑动速度。
- *
- * @return 滑动速度,以每秒钟移动了多少像素值为单位。
- */
- private int getScrollVelocity() {
- mVelocityTracker.computeCurrentVelocity(1000);
- int velocity = (int) mVelocityTracker.getXVelocity();
- return Math.abs(velocity);
- }
- /**
- * 回收VelocityTracker对象。
- */
- private void recycleVelocityTracker() {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- class ScrollTask extends AsyncTask<Integer, Integer, Integer> {
- @Override
- protected Integer doInBackground(Integer... speed) {
- int leftMargin = leftLayoutParams.leftMargin;
- // 根据传入的速度来滚动界面,当滚动到达左边界或右边界时,跳出循环。
- while (true) {
- leftMargin = leftMargin + speed[0];
- if (leftMargin > rightEdge) {
- leftMargin = rightEdge;
- break;
- }
- if (leftMargin < leftEdge) {
- leftMargin = leftEdge;
- break;
- }
- publishProgress(leftMargin);
- // 为了要有滚动效果产生,每次循环使线程睡眠20毫秒,这样肉眼才能够看到滚动动画。
- sleep(20);
- }
- if (speed[0] > 0) {
- isLeftLayoutVisible = true;
- } else {
- isLeftLayoutVisible = false;
- }
- return leftMargin;
- }
- @Override
- protected void onProgressUpdate(Integer... leftMargin) {
- leftLayoutParams.leftMargin = leftMargin[0];
- leftLayout.setLayoutParams(leftLayoutParams);
- }
- @Override
- protected void onPostExecute(Integer leftMargin) {
- leftLayoutParams.leftMargin = leftMargin;
- leftLayout.setLayoutParams(leftLayoutParams);
- }
- }
- /**
- * 使当前线程睡眠指定的毫秒数。
- *
- * @param millis
- * 指定当前线程睡眠多久,以毫秒为单位
- */
- private void sleep(long millis) {
- try {
- Thread.sleep(millis);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
接着我来说明一下和以前不同的部分。我们可以看到,这里将onLayout方法进行了重写,使用getChildAt(0)获取到的布局作为左边布局,使用getChildAt(1)获取到的布局作为右边布局。并将左边布局的宽度重定义为屏幕宽度减去leftLayoutPadding,将右侧布局的宽度重定义为屏幕宽度。然后让左边布局偏移出屏幕,这样能看到的就只有右边布局了。因此在这里我们也可以看出,使用SlidingLayout这个布局的前提条件,必须为这个布局提供两个子元素,第一个元素会作为左边布局偏移出屏幕,第二个元素会作为右边布局显示在屏幕上。
然后我们看一下setScrollEvent方法,这个方法接收一个View作为参数,然后为这个View绑定了一个touch事件。这是什么意思呢?让我们来想象一个场景,如果右侧布局是一个LinearLayout,我可以通过监听LinearLayout上的touch事件来控制左侧布局的显示和隐藏。但是如果右侧布局的LinearLayout里面加入了一个ListView,而这个ListView又充满了整个LinearLayout,这个时候LinearLayout将不可能再被touch到了,这个时候我们就需要将touch事件注册到ListView上。setScrollEvent方法也就是提供了一个注册接口,touch事件将会注册到传入的View上。
最后还有一个陌生的方法,isBindBasicLayout。这个方法就是判断了一下注册touch事件的View是不是四个基本布局之一,如果是就返回true,否则返回false。这个方法在整个SlidingLayout中起着非常重要的作用,主要用于控制onTouch事件是返回true还是false,这将影响到布局当中的View的功能是否可用。由于里面牵扯到了Android的事件转发机制,内容比较多,就不在这里详细解释了,我会考虑以后专门写一篇文章来介绍Android的事件机制。这里就先简单记住如果是基本布局就返回true,否则就返回false。
好了,我们的SlidingLayout写完了,接下来就是见证奇迹的时刻,让我们一起看看如何一分钟在Activity中引入滑动菜单功能。
创建或打开layout目录下的activity_main.xml文件,加入如下代码:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="horizontal"
- tools:context=".MainActivity" >
- <!-- 使用自定义的侧滑布局,orientation必须为水平方向 -->
- <com.example.slide.SlidingLayout
- android:id="@+id/slidingLayout"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="horizontal" >
- <!--
- 侧滑布局的根节点下,有且只能有两个子元素,这两个子元素必须是四种基本布局之一,
- 即LinearLayout, RelativeLayout, FrameLayout或TableLayout。
- 第一个子元素将做为左侧布局,初始化后被隐藏。第二个子元素将做为右侧布局,
- 也就是当前Activity的主布局,将主要的数据放在里面。
- -->
- <RelativeLayout
- android:id="@+id/menu"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:background="#00ccff" >
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:text="This is menu"
- android:textColor="#000000"
- android:textSize="28sp" />
- </RelativeLayout>
- <LinearLayout
- android:id="@+id/content"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical" >
- <Button
- android:id="@+id/menuButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Menu" />
- <ListView
- android:id="@+id/contentList"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- </ListView>
- </LinearLayout>
- </com.example.slide.SlidingLayout>
- </LinearLayout>
然后创建或打开MainActivity作为程序的主Activity,加入代码:
- public class MainActivity extends Activity {
- /**
- * 侧滑布局对象,用于通过手指滑动将左侧的菜单布局进行显示或隐藏。
- */
- private SlidingLayout slidingLayout;
- /**
- * menu按钮,点击按钮展示左侧布局,再点击一次隐藏左侧布局。
- */
- private Button menuButton;
- /**
- * 放在content布局中的ListView。
- */
- private ListView contentListView;
- /**
- * 作用于contentListView的适配器。
- */
- private ArrayAdapter<String> contentListAdapter;
- /**
- * 用于填充contentListAdapter的数据源。
- */
- private String[] contentItems = { "Content Item 1", "Content Item 2", "Content Item 3",
- "Content Item 4", "Content Item 5", "Content Item 6", "Content Item 7",
- "Content Item 8", "Content Item 9", "Content Item 10", "Content Item 11",
- "Content Item 12", "Content Item 13", "Content Item 14", "Content Item 15",
- "Content Item 16" };
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- slidingLayout = (SlidingLayout) findViewById(R.id.slidingLayout);
- menuButton = (Button) findViewById(R.id.menuButton);
- contentListView = (ListView) findViewById(R.id.contentList);
- contentListAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
- contentItems);
- contentListView.setAdapter(contentListAdapter);
- // 将监听滑动事件绑定在contentListView上
- slidingLayout.setScrollEvent(contentListView);
- menuButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- // 实现点击一下menu展示左侧布局,再点击一下隐藏左侧布局的功能
- if (slidingLayout.isLeftLayoutVisible()) {
- slidingLayout.scrollToRightLayout();
- } else {
- slidingLayout.scrollToLeftLayout();
- }
- }
- });
- }
- }
最后还是老规矩,给出AndroidManifest.xml的代码:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.example.slide"
- android:versionCode="1"
- android:versionName="1.0" >
- <uses-sdk
- android:minSdkVersion="8"
- android:targetSdkVersion="8" />
- <application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@android:style/Theme.NoTitleBar" >
- <activity
- android:name="com.example.slide.MainActivity"
- android:label="@string/app_name" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
- </manifest>
而当左边布局完全显示的时候,效果图如下:
除此之外,点击Menu按钮也可以控制左边布局的显示和隐藏,大家可以自己试一下。
使用自定义布局的话,就可以用简单的方式在任意Activity中加入滑动菜单功能,即使你有再多的Activity也不用怕了,一分钟引入滑动菜单妥妥的。
再总结一下吧,向Activity中加入滑动菜单功能只需要两步:
1. 在Acitivty的layout中引入我们自定义的布局,并且给这个布局要加入两个直接子元素。
2. 在Activity中通过setScrollEvent方法,给一个View注册touch事件。
好了,今天的讲解到此结束,有疑问的朋友请在下面留言。
补充:
由于这段文章写的比较早了,那时写的滑动菜单还存在着不少问题,我之后又将上面的代码做了不少改动,编写了一个修正版的滑动菜单,这个版本主要修正了以下内容:
1.将滑动方式改成了覆盖型。
2.ListView上下滚动时不会轻易滑出菜单。
3.正在滑动时屏蔽掉内容布局上的事件。
4.当菜单布局展示时,点击一下右侧的内容布局,可以将菜单隐藏。
5.修复刚打开程序时,菜单可能会短暂显示一下,然后瞬间消失的bug。
另外,有对双向滑动菜单感兴趣的朋友请转阅 Android双向滑动菜单完全解析,教你如何一分钟实现双向滑动特效。