Android侧滑菜单完整详细示例(改进版)

简介: MainActivity如下:package cc.cd;import android.os.Bundle;import android.view.
MainActivity如下:
package cc.cd;

import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import android.app.Activity;
import android.content.Context;
/**
 * Demo描述:
 * 将Patience5中使用的侧滑菜单封装为一个自定义控件从而便于复用
 * 
 * 该示例在Patience5的不同之处:
 * 1 Patience5中采用的是LinearLayout布局,而该自定义控件继承自RelativeLayout
 * 2 在Patience5中是不断改变menuView的leftMargin而在此处是不断改变contentView的
 *   rightMargin.这一点在阅读代码时要注意.
 *   
 * 参考资料:
 * http://blog.csdn.net/guolin_blog/article/details/8744400
 * Thank you very much
 */
public class MainActivity extends Activity {
	private Context mContext;
	private ListView mContentListView;
    private Button mContentMenuButton;
    private SlidingMenuRelativeLayout mSlidingMenuRelativeLayout;
    private String [] listViewItems=new String [20];
    private ArrayAdapter<String> mArrayAdapter;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		init();
	}
	
	private void init(){
		mContext=this;
		mContentListView=(ListView) findViewById(R.id.contentListView);
		mContentMenuButton=(Button) findViewById(R.id.contentMenuButton);
		mContentMenuButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View view) {
				boolean isMenuVisible=mSlidingMenuRelativeLayout.isMenuVisible();
				if (isMenuVisible) {
					mSlidingMenuRelativeLayout.scrollToContent();
				} else {
					mSlidingMenuRelativeLayout.scrollToMenu();
				}
			}
		});
		mSlidingMenuRelativeLayout=(SlidingMenuRelativeLayout) findViewById(R.id.slidingMenuRelativeLayout);
		//滑动事件绑定在contentListView上
		mSlidingMenuRelativeLayout.setBindView(mContentListView);
		initListViewData();
		mArrayAdapter=new ArrayAdapter<String>(mContext, android.R.layout.simple_list_item_1, listViewItems);
		mContentListView.setAdapter(mArrayAdapter);
		mContentListView.setOnItemClickListener(new OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
				String item = listViewItems[position];
				Toast.makeText(mContext, item, Toast.LENGTH_SHORT).show();
			}
		});
	}
	
	private void initListViewData(){
		String temp=null;
		for (int i = 0; i < 20; i++) {
			temp="This is "+i;
			listViewItems[i]=temp;
		}
	}

}


SlidingMenuRelativeLayout如下:

package cc.cd;

import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.Toast;
/**
 * 示例说明:
 * 1 该自定义控件继承自RelativeLayout
 * 2 在布局文件中有两部分menuView和contentView.因为是RelativeLayout布局
 *   所以这两者是重叠的.
 * 3 通过不断改变contentView的rightMargin值来显示和隐藏menuView
 * 4 请注意在xml布局文件中设置了contentView对齐方式为layout_alignParentRight
 *   设置了menuView对齐方式为 android:layout_alignParentLeft.
 *   所以注意代码:
 *   //设置contentView的最小和最大rightMargin值
 *   contentParamsMinRightMargin=-mMenuLayoutParams.width;
 *   contentParamsMaxRightMargin=0;
 *   这个还是挺巧妙的.因为contentView是相对于父控件右对齐的.所以在原始状态下
 *   他的rightMargin的值为0.当手指按在contentView上滑向屏幕的右侧时可以不断减小
 *   它的rightMargin,从而导致contentView移向屏幕的右侧本来被其遮盖的menuView
 *   也就随之显现.
 *   所以contentView的最大rightMargin值=0,这个也就是我们进入App后看到的:
 *   显示了contentView,遮盖了menuView.它的rightMargin=0;与父控件右侧对齐.
 *   当contentView移向屏幕的右边时,它的rightMargin在逐渐减小直到rightMargin
 *   的绝对值对于menuView的宽度.
 *   
 *  
 *  代码细节:
 *  1 mMenuLayoutParams和mContentLayoutParams都是
 *    MarginLayoutParams类型的,因为menuView和conentView的父控件
 *    是自定义的SlidingMenuRelativeLayout,而不是常用的系统XXXLayout
 *  2 在手指在ListView上滑动然后抬起,有时会出现ListView的item被按下且一直没有
 *    弹起的情况.造成该现象的原因暂时不明,但可用unFocusBindView()方法使得
 *    ListView失去焦点.该方法在示例中两次被调用.可以将其注释后观察效果.
 *    关于这点,在代码中仍然存在小bug.需以后继续优化.
 */
public class SlidingMenuRelativeLayout extends RelativeLayout {
	private int screenWidth;
	private View contentView;
	private View menuView;
	private View mBindView;
	private float xDown;
	private float xMove;
	private float xUp;
	private float yDown;
	private float yMove;
	private float yUp;
	private Button mButton;
	private Context mContext;
	// 当前是否在滑动
	private boolean isSliding;
	// 在被判定为滚动之前用户手指可移动的最大值
	private int scaledTouchSlop;
	// menu是否可见的标志位,该值在滑动过程中无效.
	// 只有在滑动结束后,完全显示或隐藏menu时才会更改此值
	private boolean isMenuVisible = false;
	private int contentParamsMaxRightMargin = 0;
	private int contentParamsMinRightMargin = 0;
	// 速度追踪
	private VelocityTracker mVelocityTracker;
	// 阈值
	public static final int VELOCITY_THRESHOLD = 200;
	// TAG
	private final static String TAG = "SlidingMenuRelativeLayout";
	// menu的布局LayoutParams
	private MarginLayoutParams mMenuLayoutParams;
	// content的布局LayoutParams
	private MarginLayoutParams mContentLayoutParams;

	public SlidingMenuRelativeLayout(Context context) {
		super(context);
	}

	public SlidingMenuRelativeLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	public SlidingMenuRelativeLayout(Context context, AttributeSet attrs,
			int defStyle) {
		super(context, attrs, defStyle);
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (changed) {
			// 初始化contentView
			contentView = getChildAt(1);
			mContentLayoutParams = (MarginLayoutParams) contentView.getLayoutParams();
			// 将contentView的宽度设置为屏幕的宽度
			mContentLayoutParams.width = screenWidth;
			contentView.setLayoutParams(mContentLayoutParams);

			// 初始化menuView
			menuView = getChildAt(0);
			mMenuLayoutParams = (MarginLayoutParams) menuView.getLayoutParams();
			// 设置contentView的最小和最大rightMargin值.
			contentParamsMinRightMargin = -mMenuLayoutParams.width;
			contentParamsMaxRightMargin = 0;

			mButton = (Button) menuView.findViewById(R.id.menuButton);
			mButton.setOnClickListener(new OnClickListener() {
				@Override
				public void onClick(View view) {
					Toast.makeText(mContext, "Hello", Toast.LENGTH_SHORT).show();
				}
			});
		}
	}

	private void init(Context context) {
		mContext = context;
		// 获取屏幕宽度
		WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
		screenWidth = windowManager.getDefaultDisplay().getWidth();
		// 获取ScaledTouchSlop
		scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
	}

	/**
	 * 注册处理Touch事件的View 在改示例中处理了ListView的Touch来显示和隐藏menuView的.
	 * 在实际开发中可以依据需求设置其他为其他控件
	 */
	public void setBindView(View bindView) {
		mBindView = bindView;
		mBindView.setOnTouchListener(new TouchListenerImpl());
	}

	// 使BindView失去焦点
	public void unFocusBindView() {
		if (mBindView != null) {
			mBindView.setPressed(false);
			mBindView.setFocusable(false);
			mBindView.setFocusableInTouchMode(false);
		}
	}

	private class TouchListenerImpl implements OnTouchListener {
		@Override
		public boolean onTouch(View v, MotionEvent event) {
			// 开始速度追踪
			startVelocityTracker(event);
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				xDown = event.getRawX();
				yDown = event.getRawY();
				break;
			case MotionEvent.ACTION_MOVE:
				xMove = event.getRawX();
				yMove = event.getRawY();
				int distanceX = (int) (xMove - xDown);
				int distanceY = (int) (yMove - yDown);
				// 手指滑向屏幕右侧,distanceX为正数
				if (!isMenuVisible&& distanceX >= scaledTouchSlop
					&& (isSliding || Math.abs(distanceY) <= scaledTouchSlop)) {
					isSliding = true;
					mContentLayoutParams.rightMargin = -distanceX;
					// 处理越界的情况
					if (mContentLayoutParams.rightMargin < contentParamsMinRightMargin) {
						mContentLayoutParams.rightMargin = contentParamsMinRightMargin;
					}
					// 设置contentView的LayoutParams
					contentView.setLayoutParams(mContentLayoutParams);
				}

				// 手指滑向屏幕左侧,distanceX为负数
				if (isMenuVisible && -distanceX >= scaledTouchSlop) {
					isSliding = true;
					mContentLayoutParams.rightMargin = contentParamsMinRightMargin - distanceX;
					// 处理越界的情况
					if (mContentLayoutParams.rightMargin > contentParamsMaxRightMargin) {
						mContentLayoutParams.rightMargin = contentParamsMaxRightMargin;
					}
					// 设置contentView的LayoutParams
					contentView.setLayoutParams(mContentLayoutParams);
				}
				break;
			case MotionEvent.ACTION_UP:
				xUp = event.getRawX();
				int upDistanceX = (int) (xUp - xDown);
				if (isSliding) {
					// 判断手势意图想显示menu
					if (wantToShowMenu()) {
						// 判断是否显示menu
						if (shouldScrollToMenu()) {
							scrollToMenu();
						} else {
							scrollToContent();
						}
					}

					// 判断手势意图想显示content
					if (wantToShowContent()) {
						// 判断是否显示content
						if (shouldScrollToContent()) {
							scrollToContent();
						} else {
							scrollToMenu();
						}
					}
				} else {
					// 当完全显示菜单界面时,点击仅能看到的contentView可将其完全显示
					if (upDistanceX < scaledTouchSlop && isMenuVisible) {
						scrollToContent();
					}
				}
				// 终止速度追踪
				stopVelocityTracker();
				break;
			default:
				break;
			}

			/**
			 * 在处理完DOWN,MOVE,UP后进行该if...else判断. 
			 * 1 如果不是在滑动状态,则返回false.否则ListView无法滑动且其Item无法点击. 
			 * 2 若在滑动,则让ListView失去焦点.
			 */
			if (!isSliding) {
				return false;
			} else {
				unFocusBindView();
			}
			return true;
		}

	}

	/**
	 * 判断当前手势是否想显示菜单Menu 
	 * 判断条件: 
	 * 1 抬起坐标大于按下坐标 
	 * 2 menu本身不可见
	 */
	private boolean wantToShowMenu() {
		return ((xUp - xDown > 0) && (!isMenuVisible));
	}

	/**
	 * 判断是否应该将menu完整显示出来 
	 * 判断条件: 滑动距离大于菜单的二分之一 
	 *        或者滑动速度大于速度阈值VELOCITY_THRESHOLD
	 */
	private boolean shouldScrollToMenu() {
		return ((xUp - xDown > mMenuLayoutParams.width / 2) || (getScrollVelocity() > VELOCITY_THRESHOLD));
	}

	/**
	 * 将屏幕滚动到menu.即将menu完整显示.
	 */
	public void scrollToMenu() {
		new ScrollAsyncTask().execute(-30);
	}

	/**
	 * 判断当前手势是否想显示菜单Content 
	 * 判断条件: 
	 * 1 抬起坐标小于按下坐标
	 * 2 menu本身可见
	 */
	private boolean wantToShowContent() {
		return ((xUp - xDown < 0) && (isMenuVisible));
	}

	/**
	 * 判断是否应该将content完整显示出来 
	 * 判断条件: 滑动距离大于菜单的二分之一 
	 *        或者滑动速度大于速度阈值VELOCITY_THRESHOLD
	 */
	private boolean shouldScrollToContent() {
		return ((xDown - xUp > mMenuLayoutParams.width / 2) || (getScrollVelocity() > VELOCITY_THRESHOLD));
	}

	/**
	 * 将屏幕滚动到content.即将content完整显示
	 */
	public void scrollToContent() {
		new ScrollAsyncTask().execute(30);
	}

	public boolean isMenuVisible() {
		return isMenuVisible;
	}

	/**
	 * 开始速度追踪
	 */
	private void startVelocityTracker(MotionEvent event) {
		if (mVelocityTracker == null) {
			mVelocityTracker = VelocityTracker.obtain();
		}
		mVelocityTracker.addMovement(event);
	}

	/**
	 * 获取在content上X方向的手指滑动速度
	 */
	private int getScrollVelocity() {
		// 设置VelocityTracker单位.1000表示1秒时间内运动的像素
		mVelocityTracker.computeCurrentVelocity(1000);
		// 获取在1秒内X方向所滑动像素值
		int xVelocity = (int) mVelocityTracker.getXVelocity();
		return Math.abs(xVelocity);
	}

	/**
	 * 终止速度追踪
	 */
	private void stopVelocityTracker() {
		if (mVelocityTracker != null) {
			mVelocityTracker.recycle();
			mVelocityTracker = null;
		}
	}

	/**
	 * 利用异步任务不断修改contentView的LayoutParams中的rightMargin从而达到 contentView视图移动的效果
	 */
	private class ScrollAsyncTask extends AsyncTask<Integer, Integer, Integer> {
		@Override
		protected Integer doInBackground(Integer... speed) {
			int contentLayoutParamsRightMargin = mContentLayoutParams.rightMargin;
			while (true) {
				// 每次变化的speed
				contentLayoutParamsRightMargin = contentLayoutParamsRightMargin + speed[0];
				// 若越界,则处理越界且跳出循环
				if (contentLayoutParamsRightMargin > contentParamsMaxRightMargin) {
					contentLayoutParamsRightMargin = contentParamsMaxRightMargin;
					break;
				}
				// 若越界,则处理越界且跳出循环
				if (contentLayoutParamsRightMargin < contentParamsMinRightMargin) {
					contentLayoutParamsRightMargin = contentParamsMinRightMargin;
					break;
				}
				// 通知进度更新
				publishProgress(contentLayoutParamsRightMargin);
				// 线程睡眠15毫秒,便于体现滚动效果
				try {
					Thread.sleep(15);
				} catch (Exception e) {
				}
			}

			// 依据滑动的速度设置标志位isMenuVisible
			if (speed[0] > 0) {
				isMenuVisible = false;
			} else {
				isMenuVisible = true;
			}
			isSliding = false;
			return contentLayoutParamsRightMargin;
		}

		@Override
		protected void onProgressUpdate(Integer... rightMargin) {
			super.onProgressUpdate(rightMargin);
			mContentLayoutParams.rightMargin = rightMargin[0];
			contentView.setLayoutParams(mContentLayoutParams);
			unFocusBindView();
		}

		@Override
		protected void onPostExecute(Integer rightMargin) {
			super.onPostExecute(rightMargin);
			mContentLayoutParams.rightMargin = rightMargin;
			contentView.setLayoutParams(mContentLayoutParams);
		}
	}

}

main.xml如下:

<cc.cd.SlidingMenuRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/slidingMenuRelativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout
        android:id="@+id/menuRelativeLayout"
        android:layout_width="270dip"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:background="#00ccff">

        <TextView
            android:id="@+id/menuTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:text="This is Menu"
            android:textColor="#000000"
            android:textSize="25sp" />
        <Button
             android:id="@+id/menuButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Click here"
            android:textColor="#000000"
            android:textSize="25sp"
            />
    </RelativeLayout>

    <LinearLayout
        android:id="@+id/contentLinearLayout"
        android:layout_width="320dip"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:background="#e9e9e9"
        android:orientation="vertical" >

        <Button
            android:id="@+id/contentMenuButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Menu" />

        <ListView
            android:id="@+id/contentListView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:cacheColorHint="#00000000" />
    </LinearLayout>

</cc.cd.SlidingMenuRelativeLayout>


相关文章
|
6月前
|
存储 算法 开发工具
OpenCV 安卓编程示例:1~6 全
OpenCV 安卓编程示例:1~6 全
149 0
|
编解码 监控 API
Android平台GB28181设备接入侧音频采集推送示例
GB/T28181是广泛应用于视频监控行业的标准协议规范,可以在不同设备之间实现互联互通。今天我们主要探讨Android平台的Audio采集部分。
123 1
|
3月前
|
XML API Android开发
码农之重学安卓:利用androidx.preference 快速创建一、二级设置菜单(demo)
本文介绍了如何使用androidx.preference库快速创建具有一级和二级菜单的Android设置界面的步骤和示例代码。
97 1
码农之重学安卓:利用androidx.preference 快速创建一、二级设置菜单(demo)
|
15天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
22 1
|
5月前
|
XML Java Android开发
34. 【Android教程】菜单:Menu
34. 【Android教程】菜单:Menu
110 2
|
5月前
|
开发工具 Android开发 开发者
Android Studio中两个让初学者崩溃菜单
Android Studio中两个让初学者崩溃菜单
54 0
|
6月前
|
XML Android开发 数据格式
android 12 添加菜单
android 12 添加菜单
40 0
|
3月前
|
API Android开发
Android使用AlertDialog实现弹出菜单
本文分享了在Android开发中使用AlertDialog实现弹出菜单的方法,并通过代码示例和错误处理,展示了如何避免因资源ID找不到导致的crash问题。
59 1
|
5月前
|
API Android开发 容器
36. 【Android教程】侧滑菜单:DrawerLayout
36. 【Android教程】侧滑菜单:DrawerLayout
73 1
|
5月前
|
开发工具 Android开发
技术经验分享:Android编译命令m、mm、mmm区别及工程搭建示例
技术经验分享:Android编译命令m、mm、mmm区别及工程搭建示例
281 0