標籤:
可移動頁面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;@Overrideprotected 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")@Overridepublic 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;}@Overridepublic 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);}}}@Overridepublic 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實現側滑的效果:
滑出菜單SlidingMenuSlidingMenu開發步驟前面說的兩個側滑效果,都依賴於手勢觸摸事件,實際開發中由於頁面上很多控制項都要響應點擊事件,其實不可能一一接管頁面觸摸事件。問題的癥結在於菜單布局和內容布局都在同一個頁面中,所以極易造成滑動衝突,要想徹底解決滑動衝突,最好還是把兩種布局分開到不同頁面處理,技術上便是使用不同的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使用SlidingMenuViewPager本身做翻頁操作時就使用了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;@Overridepublic 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();@Overridepublic 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;}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {mContext = getActivity();mView = inflater.inflate(mLayoutId, null);return mView;}@Overridepublic void onListItemClick(ListView lv, View v, int position, long id) {if (onSlidingMenuListener != null) {onSlidingMenuListener.onMenuItemClick(position);}}}
點此查看Android開發筆記的完整目錄
Android開發筆記(一百零一)滑出式菜單