Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發筆記(一百零一)滑出式菜單

Android開發筆記(一百零一)滑出式菜單

編輯:關於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 = 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);
		}
	}
	
}
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved