Android开发笔记(一百零一)滑出式菜单

可移动页面MoveActivity

滑出式菜单从界面上看,像极了一个水平滚动视图HorizontalScrollView,当然也可以使用HorizontalScrollView来实现侧滑菜单。不过今天博主要说的是利用线性布局LinearLayout来实现,而且是水平方向上的线性布局。

可是LinearLayout作为水平展示时有点逗,因为如果下面有两个子视图的宽度都是match_parent,那么LinearLayout只会显示第一个子视图,第二个子视图却是怎么拉也死活显示不了。倘若在外侧加个HorizontalScrollView,由于HorizontalScrollView的宽度只能是wrap_content,因此子视图的宽度也只能是wrap_content而不能是match_parent了,故而HorizontalScrollView做不到子页面全屏的效果。

现在我们既希望两个子视图的宽度是match_parent,又希望能够拖动两个子视图,还有没有办法呢?办法肯定是有的,在《Android开发笔记(三十五)页面布局视图》中,我们提到margin和padding都可用来设置空隙,空隙的数值都是正数,其实空隙值也能是负数,负数表示该视图被隐藏了一部分,仿佛一张纸插了部分纸面到书中,于是只有一部分露了出来。具体到LinearLayout的编码实现,对应的便是LinearLayout.LayoutParams的leftMargin参数,若该参数为正数,则视图页面拉出了一段空白;若该参数为负数,则视图页面隐藏了一段内容;若该参数是该视图宽度的赋值,则表示视图页面完全隐藏了起来,跟visible="gone"的效果类似。

所以我们可以给视图添加触摸监听器OnTouchListener,在触摸坐标发生变化的同时,给菜单子页面隐入隐出对应的宽度,从而达到抽屉式拉出菜单的效果。一旦触摸弹起,根据手势滑动的距离,判断当前是要拉出整个菜单,还是缩回才拉出一部分的菜单。这个判断可按照滑动偏移是否达到屏幕一半宽度的条件,至于自动拉出或者自动缩进的动画,可由Runnable来定时刷新视图的leftMargin参数。

下面是一个简单侧滑的效果截图:

下面是一个简单侧滑的代码例子:

import com.example.exmslidingmenu.util.MetricsUtil;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.LinearLayout;

public class MoveActivity extends Activity implements OnTouchListener,OnClickListener {
	private static final String TAG = "MoveActivity";

	private int screenWidth;
	private float rawX=0;
	private LinearLayout.LayoutParams menuParams;
	private View ll_menu_move;
	private View ll_content_move;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_move);
		initView();
	}

	@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
	private void initView() {
		ll_menu_move = (View) findViewById(R.id.ll_menu_move);
		ll_content_move = (View) findViewById(R.id.ll_content_move);

		screenWidth = MetricsUtil.getWidth(this);
		menuParams = (LinearLayout.LayoutParams) ll_menu_move.getLayoutParams();
		menuParams.width = screenWidth;
		menuParams.leftMargin = -screenWidth;
		ll_content_move.getLayoutParams().width = screenWidth;
		ll_menu_move.setOnClickListener(this);
		ll_content_move.setOnTouchListener(this);
	}

	@SuppressLint("ClickableViewAccessibility")
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		int distanceX = (int) (event.getRawX() - rawX);
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			rawX = event.getRawX();
			break;
		case MotionEvent.ACTION_MOVE:
			if (distanceX > 0) {
				menuParams.leftMargin = -screenWidth + distanceX;
				ll_menu_move.setLayoutParams(menuParams);
			}
			break;
		case MotionEvent.ACTION_UP:
			if (distanceX < screenWidth/2) {
				mHandler.postDelayed(new ScrollRunnable(-1, distanceX), mTimeGap);
			} else {
				mHandler.postDelayed(new ScrollRunnable(1, distanceX), mTimeGap);
			}
			break;
		}
		return true;
	}

	private int mTimeGap = 20;
	private int mDistanceGap = 20;
	private Handler mHandler = new Handler();
	private class ScrollRunnable implements Runnable {
		private int mDirection;
		private int mDistance;
		public ScrollRunnable(int direction, int distance) {
			mDirection = direction;
			mDistance = distance;
		}

		@Override
		public void run() {
			if (mDirection==-1 && mDistance>0) {
				mDistance -= mDistanceGap;
				if (mDistance < 0) {
					mDistance = 0;
				}
				menuParams.leftMargin = -screenWidth + mDistance;
				ll_menu_move.setLayoutParams(menuParams);
				mHandler.postDelayed(new ScrollRunnable(-1, mDistance), mTimeGap);
			} else if (mDirection==1 && mDistance<screenWidth) {
				mDistance += mDistanceGap;
				if (mDistance > screenWidth) {
					mDistance = screenWidth;
				}
				menuParams.leftMargin = -screenWidth + mDistance;
				ll_menu_move.setLayoutParams(menuParams);
				mHandler.postDelayed(new ScrollRunnable(1, mDistance), mTimeGap);
			}
		}
	}
	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.ll_menu_move) {
			menuParams.leftMargin = -screenWidth;
			ll_menu_move.setLayoutParams(menuParams);
		}
	}

}

水平列表视图HorizontalListView

上面说的侧滑菜单只适用于单个Activity页面,如果要在其他页面也使用侧滑菜单,显然是不方便的。基于此,我们希望把侧滑功能独立出来,封装成一个通用的控件。现在有个开源的HorizontalListView,它是水平滚动的列表视图,如果该视图只有两列,左边一列作为菜单页面,右边一列作为内容页面,这就很类似侧滑菜单的功能。

当然,要把HorizontalListView作为侧滑菜单来使用,我们还需要对其做下列改造:

1、在手势松开的时候,根据当前的滑动偏移,自动判断接下来是往左滑动对齐,还是往右滑动对齐。具体步骤就是:首先在onTouch方法中拦截MotionEvent.ACTION_UP与MotionEvent.ACTION_CANCE进行判断;其次计算当前的滑动偏移,如果滑动距离超过阈值,则继续翻页滑动,否则做滑动缩回;最后调用Scroller的startScroll方法来完成后续的滑动动画效果。

2、菜单默认在左边页,内容默认在右边页,所以首次加载视图时,页面要自动滑到右边的内容页(调用scrollTo方法滚动到内容页)。

3、通过手势滑动拉出菜单页后,要捕获点击事件完成翻页,即在onSingleTapUp方法中将当前页面切换到内容页。

下面是采用HorizontalListView实现侧滑的效果截图:

滑出菜单SlidingMenu

SlidingMenu开发步骤

前面说的两个侧滑效果,都依赖于手势触摸事件,实际开发中由于页面上很多控件都要响应点击事件,其实不可能一一接管页面触摸事件。问题的症结在于菜单布局和内容布局都在同一个页面中,所以极易造成滑动冲突,要想彻底解决滑动冲突,最好还是把两种布局分开到不同页面处理,技术上便是使用不同的Fragment分别放置菜单和内容布局。SlidingMenu就是采用这一思路的开源库,也是使用最广泛的滑出式菜单控件。

使用SlidingMenu的开发步骤大致如下:

1、给自己的工程引用SlidingMenu库工程;

2、写个继承自SlidingFragmentActivity的Activity类;

3、调用setContentView方法设置内容布局,调用setBehindContentView方法设置菜单布局,注意两个初始布局都是空的;

4、从自己写的Fragment类分别构造出实际的内容布局和菜单布局,然后调用FragmentManager的replace方法把初始布局替换为实际布局;

5、调用getSlidingMenu()获得侧滑菜单的实例,并设置侧滑菜单的显示参数;

SlidingMenu参数设置

下面是SlidingMenu常用的参数设置:

setSlidingEnabled : 设置是否允许滑动。

setMode : 设置滑出模式。LEFT表示左侧菜单,RIGHT表示右侧菜单,LEFT_RIGHT表示左右两侧都有菜单。

setTouchModeAbove : 设置触摸范围。TOUCHMODE_MARGIN表示只在空白处响应触摸,TOUCHMODE_FULLSCREEN表示全屏均响应触摸,TOUCHMODE_NONE表示不响应触摸。

setBehindOffsetRes : 设置菜单布局相对于页面的偏移。

setBehindScrollScale : 设置滚动条的缩放比例。

setFadeDegree : 设置淡入淡出的度数。

setShadowWidthRes : 设置阴影的宽度。

setShadowDrawable : 设置背景图像。

setSecondaryMenu : 设置第二个菜单布局。setMode为LEFT_RIGHT时使用。

setSecondaryShadowDrawable : 设置第二个菜单的背景图像。setMode为LEFT_RIGHT时使用。

菜单点击时跳回内容页面

菜单点击的交互例子可见demo工程的ResponsiveUIActivity,主要做法步骤如下:

1、定义一个菜单点击接口如OnSlidingMenuListener,其内部定义菜单点击方法如onMenuItemClick;

2、菜单Fragment类定义OnSlidingMenuListener的实例,及该实例的设置方法setOnSlidingMenuListener;

3、菜单布局的Fragment类继承自ListFragment;

4、菜单Fragment类在onCreateView中调用setListAdapter方法设置菜单项列表信息;

5、重写菜单Fragment类的onListItemClick方法,收到点击事件后调用onMenuItemClick;

6、Activity类实现接口OnSlidingMenuListener,并重写onMenuItemClick方法进行相应的业务逻辑处理;

7、Activity类构造菜单布局后,对菜单布局设置点击接口setOnSlidingMenuListener(this);

ViewPager使用SlidingMenu

ViewPager本身做翻页操作时就使用了Fragment,然后SlidingMenu也采用Fragment区分菜单布局和内容布局,因此如果把ViewPager作为内容布局,就会产生Fragment嵌套的情况。即ViewPager自身就是作为内容布局的Fragment嵌入到SlidingMenu中,然后ViewPager的子页面也是作为Fragment嵌入到ViewPager,这样就造成了一个问题:Fragment嵌套可能导致资源回收异常。

表现在界面上,就是点击菜单布局后回到ViewPager页面,会看到ViewPager的头两页变空白了,查看日志发现头两页不会执行onCreateView方法。这就涉及到Fragment的回收机制,onCreateView只会在该页面第一次打开时调用,如果该页面还未被回收,自然就不会重新创建。我们首次进入Activity页面,ViewPager的头两个页面已经执行了onCreateView;接着点击菜单项,SlidingMenu把整个内容页面的Fragment替换掉,但这时对于ViewPager的子页面来说,仅仅是做了detach操作,并没有做remove或destroy操作,也就是说,ViewPager子页面根本就没被回收;所以点击菜单重新回到替换后的ViewPager时,系统发现头两页没有回收,自然也不会再次onCreateView了。

不知道这个情况算不算Fragment的一个bug,不管怎样,系统没有自动回收嵌套的Fragment,就得我们自己手动回收了。下面就是一个回收嵌套Fragment的代码例子,先执行detach操作,再执行remove操作:

	public void cleanFragments() {
		for (Fragment fragment : mFragments) {
			mFragmentMgr
				.beginTransaction()
				.detach((ColorFragment) fragment)
				.commit();
			mFragmentMgr
				.beginTransaction()
				.remove((ColorFragment) fragment)
				.commit();
		}
	}

代码示例

限于篇幅,这里就不贴出本文的完整源码了,有需要的朋友可留下邮箱,我看到后把工程打包用邮件发过去。

下面是SlidingMenu+ViewPager的效果截图:

下面是SlidingMenu的Activity主页面代码示例:

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.View;

import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;
import com.jeremyfeinstein.slidingmenu.lib.app.SlidingFragmentActivity;

public abstract class BaseContentActivity extends SlidingFragmentActivity {

	protected Fragment mContent;
	protected Fragment mMenuLeft;
	protected Fragment mMenuRight;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		int mode = SlidingMenu.LEFT;
		Bundle bundle = getIntent().getExtras();
		if (bundle != null) {
			mode = bundle.getInt("mode", SlidingMenu.LEFT);
		}
		if (findViewById(R.id.menu_frame) == null) {
			setBehindContentView(R.layout.menu_frame);
			getSlidingMenu().setMode(mode);
			getSlidingMenu().setSlidingEnabled(true);
			getSlidingMenu().setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
		} else {
			View v = new View(this);
			setBehindContentView(v);
			getSlidingMenu().setSlidingEnabled(false);
			getSlidingMenu().setTouchModeAbove(SlidingMenu.TOUCHMODE_NONE);
		}

		if (savedInstanceState != null) {
			mContent = getSupportFragmentManager().getFragment(
					savedInstanceState, "mContent");
		}
		if (mContent == null) {
			mContent = newDefaultContent();
		}
		setFragment(R.id.content_frame, mContent);

		mMenuLeft = newMenuFragment();
		setFragment(R.id.menu_frame, mMenuLeft);

		SlidingMenu sm = getSlidingMenu();
		sm.setBehindOffsetRes(R.dimen.slidingmenu_offset);
		sm.setShadowWidthRes(R.dimen.shadow_width);
		sm.setBehindScrollScale(0.25f);
		sm.setFadeDegree(0.25f);
		if (mode == SlidingMenu.LEFT_RIGHT) {
			sm.setSecondaryMenu(R.layout.menu_frame_two);
			mMenuRight = newMenuFragment();
			setFragment(R.id.menu_frame_two, mMenuRight);
			sm.setSecondaryShadowDrawable(R.drawable.shadow_right);
		}
		sm.setShadowDrawable((mode==SlidingMenu.RIGHT)?R.drawable.shadow_right:R.drawable.shadow_left);
	}

	protected void setFragment(int resid, Fragment fragment) {
		getSupportFragmentManager()
			.beginTransaction()
			.replace(resid, fragment)
			.commit();
	}

	protected abstract Fragment newDefaultContent();

	protected abstract Fragment newMenuFragment();

	@Override
	public void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
		getSupportFragmentManager().putFragment(outState, "mContent", mContent);
	}

}

下面是SlidingMenu左侧菜单的代码示例:

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;

public class BaseMenuFragment extends ListFragment {
	protected View mView;
	protected Context mContext;

	protected OnSlidingMenuListener onSlidingMenuListener;
	public void setOnSlidingMenuListener(OnSlidingMenuListener listener) {
		this.onSlidingMenuListener = listener;
	}

	protected int mLayoutId;
	public BaseMenuFragment(int layout_id) {
		mLayoutId = layout_id;
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		mContext = getActivity();
		mView = inflater.inflate(mLayoutId, null);
		return mView;
	}

	@Override
	public void onListItemClick(ListView lv, View v, int position, long id) {
		if (onSlidingMenuListener != null) {
			onSlidingMenuListener.onMenuItemClick(position);
		}
	}

}

点此查看Android开发笔记的完整目录

时间: 2024-11-15 23:41:43

Android开发笔记(一百零一)滑出式菜单的相关文章

Android开发笔记(一百二十)两种侧滑布局

SlidingPaneLayout SlidingPaneLayout是Android在android-support-v4.jar中推出的一个可滑动面板的布局,在前面<Android开发笔记(一百零一)滑出式菜单>中,我们提到水平布局时的LinearLayout无法自动左右拉伸,必须借助于手势事件才能拉出左侧隐藏的布局,现在SlidingPaneLayout便是为了解决LinearLayout无法自动拉伸的缺陷.只要我们在布局文件的SlidingPaneLayout节点下定义两个子布局,那么

【转】Android开发笔记(序)写在前面的目录

原文:http://blog.csdn.net/aqi00/article/details/50012511 知识点分类 一方面写写自己走过的弯路掉进去的坑,避免以后再犯:另一方面希望通过分享自己的经验教训,与网友互相切磋,从而去芜存菁进一步提升自己的水平.因此博主就想,入门的东西咱就不写了,人不能老停留在入门上:其次是想拾缺补漏,写写虽然小众却又用得着的东西:另外就是想以实用为主,不求大而全,但求小而精:还有就是有的知识点是java的,只是Android开发也会经常遇上,所以蛮记下来.个人的经

Android开发笔记(一百零六)支付缴费SDK

第三方支付 第三方支付指的是第三方平台与各银行签约,在买方与卖方之间实现中介担保,从而增强了支付交易的安全性.国内常用的支付平台主要是支付宝和微信支付,其中支付宝的市场份额为71.5%,微信支付的市场份额为15.99%,也就是说这两家垄断了八分之七的支付市场(2015年数据).除此之外,还有几个app开发会用到的支付平台,包括:银联支付,主要用于公共事业缴费,如水电煤.有线电视.移动电信等等的充值:易宝支付,主要用于各种报名考试的缴费,特别是公务员与事业单位招考:快钱,被万达收购,主要用于航空旅

Android开发笔记(一百零三)地图与定位SDK

集成地图SDK 国内常用的地图SDK就是百度和高德了,二者的用法大同小异,可按照官网上的开发指南一步步来.下面是我在集成地图SDK时遇到的问题说明: 1.点击基本地图功能选项,不能打开地图,弹出"key验证出错!请在AndroidManifest.xml文件中检查key设置的"的红色字提示.查看日志提示"galaxy lib host missing meta-data,make sure you know the right way to integrate galaxy&

Android开发笔记(一百零七)统计分析SDK

APP统计分析 用户画像 对程序员来说,用户画像就是用户的属性和行为:通俗地说,用户画像是包括了个人信息.兴趣爱好.日常行为等血肉丰满的客户实体.用户画像是精准营销的产物,企业通过收集用户的行为,然后分析出用户的特征与偏好,进而挖掘潜在的商业价值,实现企业效益的最大化. 用户画像的一个具体应用是电商app的"猜你喜欢"栏目,电商平台通过对用户购买过的商品进行统计,可以分析用户日常生活用的是什么物品:电商平台还可以对用户的搜索行为.浏览行为进行统计,从中分析用户感兴趣的商品,或者说考虑购

Android开发笔记(一百零四)消息推送SDK

推送的集成 常用概念 推送:从服务器把消息实时发到客户端app上,这就是推送,推送可用于发送系统通知.发送推荐信息.发送聊天消息等等. 别名:用于给移动设备取个好记的名字,比如电脑有计算机名,可以把别名理解为开发者给移送设备起的外号.不过,多个移动设备可以起一样的别名,这几个设备就会同时收到发给该别名的消息. 标记:用于给移动设备打标签,可以理解为分类,比如超市里的泰国大米既可以打上"粮食制品"的标签,也可以打上"进口商品"的标签.服务器可以统一给某个种类的移动设备

Android开发笔记(一百零九)利用网盘实现云存储

网盘存储 个人开发者往往没有自己的后台服务器,但同时又想获取app的运行信息,这就要借助于第三方的网络存储(也叫网盘.云盘.微盘等等).通过让app自动在网盘上存取文件,可以间接实现后台服务器的存储功能,同时开发者也能及时找到app的用户信息. 曾几何时,各大公司纷纷推出免费的个人网盘服务,还开放了文件管理api给开发者调用,一时间涌现了网盘提供商的八大金刚:百度网盘.阿里云.华为网盘.腾讯微云.新浪微盘.360云盘.金山快盘.115网盘.可是好景不长,出于盈利.监管等等因素,各大网盘开放平台要

Android开发笔记(一百三十四)协调布局CoordinatorLayout

协调布局CoordinatorLayout Android自5.0之后对UI做了较大的提升,一个重大的改进是推出了MaterialDesign库,而该库的基础即为协调布局CoordinatorLayout,几乎所有的design控件都依赖于该布局.协调布局的含义,指的是内部控件互相之前的动作关联,比如在A视图的位置发生变化之时,B视图的位置也按照某种规则来变化,仿佛弹钢琴有了协奏曲一般. 使用CoordinatorLayout时,要注意以下几点:1.导入design库:2.根布局采用androi

Android开发笔记(一百三十二)矢量图形与矢量动画

矢量图形VectorDrawable 与水波图形RippleDrawable一样,矢量图形VectorDrawable也是Android5.0之后新增的图形类.矢量图不同于一般的图形,它是由一系列几何曲线构成的图像,这些曲线以数学上定义的坐标点连接而成.具体到实现上,则需开发者提供一个xml格式的矢量图形定义,然后系统根据矢量定义自动计算该图形的绘制区域.因为绘图结果是动态计算得到,所以不管缩放到多少比例,矢量图形都会一样的清晰,不像位图那样拉大后会变模糊. 矢量图形的xml定义有点复杂,其结构