可移动页面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); } } }