編輯:Android開發實例
之前我向大家介紹了史上最簡單的滑動菜單的實現方式,相信大家都還記得。如果忘記了其中的實現原理或者還沒看過的朋友,請先去看一遍之前的文章 http://www.fengfly.com/plus/view-215082-1.html,因為我們今天要實現的滑動菜單框架也是基於同樣的原理的。
之前的文章中在最後也提到了,如果是你的應用程序中有很多個Activity都需要加入滑動菜單的功能,那麼每個Activity都要寫上百行的代碼才能實現效果,再簡單的滑動菜單實現方案也沒用。因此我們今天要實現一個滑動菜單的框架,然後在任何Activity中都可以一分鐘引入滑動菜單功能。
首先還是講一下實現原理。說是滑動菜單的框架,其實說白了也很簡單,就是我們自定義一個布局,在這個自定義布局中實現好滑動菜單的功能,然後只要在Activity的布局文件裡面引入我們自定義的布局,這個Activity就擁有了滑動菜單的功能了。原理講完了,是不是很簡單?下面我們來動手實現吧。
在Eclipse中新建一個Android項目,項目名就叫做RenRenSlidingLayout。
新建一個類,名叫SlidingLayout,這個類是繼承自LinearLayout的,並且實現了OnTouchListener接口,具體代碼如下:
- public class SlidingLayout extends LinearLayout implements OnTouchListener {
- /**
- * 滾動顯示和隱藏左側布局時,手指滑動需要達到的速度。
- */
- public static final int SNAP_VELOCITY = 200;
- /**
- * 屏幕寬度值。
- */
- private int screenWidth;
- /**
- * 左側布局最多可以滑動到的左邊緣。值由左側布局的寬度來定,marginLeft到達此值之後,不能再減少。
- */
- private int leftEdge;
- /**
- * 左側布局最多可以滑動到的右邊緣。值恆為0,即marginLeft到達0之後,不能增加。
- */
- private int rightEdge = 0;
- /**
- * 左側布局完全顯示時,留給右側布局的寬度值。
- */
- private int leftLayoutPadding = 80;
- /**
- * 記錄手指按下時的橫坐標。
- */
- private float xDown;
- /**
- * 記錄手指移動時的橫坐標。
- */
- private float xMove;
- /**
- * 記錄手機抬起時的橫坐標。
- */
- private float xUp;
- /**
- * 左側布局當前是顯示還是隱藏。只有完全顯示或隱藏時才會更改此值,滑動過程中此值無效。
- */
- private boolean isLeftLayoutVisible;
- /**
- * 左側布局對象。
- */
- private View leftLayout;
- /**
- * 右側布局對象。
- */
- private View rightLayout;
- /**
- * 用於監聽側滑事件的View。
- */
- private View mBindView;
- /**
- * 左側布局的參數,通過此參數來重新確定左側布局的寬度,以及更改leftMargin的值。
- */
- private MarginLayoutParams leftLayoutParams;
- /**
- * 右側布局的參數,通過此參數來重新確定右側布局的寬度。
- */
- private MarginLayoutParams rightLayoutParams;
- /**
- * 用於計算手指滑動的速度。
- */
- private VelocityTracker mVelocityTracker;
- /**
- * 重寫SlidingLayout的構造函數,其中獲取了屏幕的寬度。
- *
- * @param context
- * @param attrs
- */
- public SlidingLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- screenWidth = wm.getDefaultDisplay().getWidth();
- }
- /**
- * 綁定監聽側滑事件的View,即在綁定的View進行滑動才可以顯示和隱藏左側布局。
- *
- * @param bindView
- * 需要綁定的View對象。
- */
- public void setScrollEvent(View bindView) {
- mBindView = bindView;
- mBindView.setOnTouchListener(this);
- }
- /**
- * 將屏幕滾動到左側布局界面,滾動速度設定為30.
- */
- public void scrollToLeftLayout() {
- new ScrollTask().execute(30);
- }
- /**
- * 將屏幕滾動到右側布局界面,滾動速度設定為-30.
- */
- public void scrollToRightLayout() {
- new ScrollTask().execute(-30);
- }
- /**
- * 左側布局是否完全顯示出來,或完全隱藏,滑動過程中此值無效。
- *
- * @return 左側布局完全顯示返回true,完全隱藏返回false。
- */
- public boolean isLeftLayoutVisible() {
- return isLeftLayoutVisible;
- }
- /**
- * 在onLayout中重新設定左側布局和右側布局的參數。
- */
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- if (changed) {
- // 獲取左側布局對象
- leftLayout = getChildAt(0);
- leftLayoutParams = (MarginLayoutParams) leftLayout.getLayoutParams();
- // 重置左側布局對象的寬度為屏幕寬度減去leftLayoutPadding
- leftLayoutParams.width = screenWidth - leftLayoutPadding;
- // 設置最左邊距為負的左側布局的寬度
- leftEdge = -leftLayoutParams.width;
- leftLayoutParams.leftMargin = leftEdge;
- leftLayout.setLayoutParams(leftLayoutParams);
- // 獲取右側布局對象
- rightLayout = getChildAt(1);
- rightLayoutParams = (MarginLayoutParams) rightLayout.getLayoutParams();
- rightLayoutParams.width = screenWidth;
- rightLayout.setLayoutParams(rightLayoutParams);
- }
- }
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- createVelocityTracker(event);
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // 手指按下時,記錄按下時的橫坐標
- xDown = event.getRawX();
- break;
- case MotionEvent.ACTION_MOVE:
- // 手指移動時,對比按下時的橫坐標,計算出移動的距離,來調整左側布局的leftMargin值,從而顯示和隱藏左側布局
- xMove = event.getRawX();
- int distanceX = (int) (xMove - xDown);
- if (isLeftLayoutVisible) {
- leftLayoutParams.leftMargin = distanceX;
- } else {
- leftLayoutParams.leftMargin = leftEdge + distanceX;
- }
- if (leftLayoutParams.leftMargin < leftEdge) {
- leftLayoutParams.leftMargin = leftEdge;
- } else if (leftLayoutParams.leftMargin > rightEdge) {
- leftLayoutParams.leftMargin = rightEdge;
- }
- leftLayout.setLayoutParams(leftLayoutParams);
- break;
- case MotionEvent.ACTION_UP:
- // 手指抬起時,進行判斷當前手勢的意圖,從而決定是滾動到左側布局,還是滾動到右側布局
- xUp = event.getRawX();
- if (wantToShowLeftLayout()) {
- if (shouldScrollToLeftLayout()) {
- scrollToLeftLayout();
- } else {
- scrollToRightLayout();
- }
- } else if (wantToShowRightLayout()) {
- if (shouldScrollToContent()) {
- scrollToRightLayout();
- } else {
- scrollToLeftLayout();
- }
- }
- recycleVelocityTracker();
- break;
- }
- return isBindBasicLayout();
- }
- /**
- * 判斷當前手勢的意圖是不是想顯示右側布局。如果手指移動的距離是負數,且當前左側布局是可見的,則認為當前手勢是想要顯示右側布局。
- *
- * @return 當前手勢想顯示右側布局返回true,否則返回false。
- */
- private boolean wantToShowRightLayout() {
- return xUp - xDown < 0 && isLeftLayoutVisible;
- }
- /**
- * 判斷當前手勢的意圖是不是想顯示左側布局。如果手指移動的距離是正數,且當前左側布局是不可見的,則認為當前手勢是想要顯示左側布局。
- *
- * @return 當前手勢想顯示左側布局返回true,否則返回false。
- */
- private boolean wantToShowLeftLayout() {
- return xUp - xDown > 0 && !isLeftLayoutVisible;
- }
- /**
- * 判斷是否應該滾動將左側布局展示出來。如果手指移動距離大於屏幕的1/2,或者手指移動速度大於SNAP_VELOCITY,
- * 就認為應該滾動將左側布局展示出來。
- *
- * @return 如果應該滾動將左側布局展示出來返回true,否則返回false。
- */
- private boolean shouldScrollToLeftLayout() {
- return xUp - xDown > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;
- }
- /**
- * 判斷是否應該滾動將右側布局展示出來。如果手指移動距離加上leftLayoutPadding大於屏幕的1/2,
- * 或者手指移動速度大於SNAP_VELOCITY, 就認為應該滾動將右側布局展示出來。
- *
- * @return 如果應該滾動將右側布局展示出來返回true,否則返回false。
- */
- private boolean shouldScrollToContent() {
- return xDown - xUp + leftLayoutPadding > screenWidth / 2
- || getScrollVelocity() > SNAP_VELOCITY;
- }
- /**
- * 判斷綁定滑動事件的View是不是一個基礎layout,不支持自定義layout,只支持四種基本layout,
- * AbsoluteLayout已被棄用。
- *
- * @return 如果綁定滑動事件的View是LinearLayout,RelativeLayout,FrameLayout,
- * TableLayout之一就返回true,否則返回false。
- */
- private boolean isBindBasicLayout() {
- if (mBindView == null) {
- return false;
- }
- String viewName = mBindView.getClass().getName();
- return viewName.equals(LinearLayout.class.getName())
- || viewName.equals(RelativeLayout.class.getName())
- || viewName.equals(FrameLayout.class.getName())
- || viewName.equals(TableLayout.class.getName());
- }
- /**
- * 創建VelocityTracker對象,並將觸摸事件加入到VelocityTracker當中。
- *
- * @param event
- * 右側布局監聽控件的滑動事件
- */
- private void createVelocityTracker(MotionEvent event) {
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- }
- /**
- * 獲取手指在右側布局的監聽View上的滑動速度。
- *
- * @return 滑動速度,以每秒鐘移動了多少像素值為單位。
- */
- private int getScrollVelocity() {
- mVelocityTracker.computeCurrentVelocity(1000);
- int velocity = (int) mVelocityTracker.getXVelocity();
- return Math.abs(velocity);
- }
- /**
- * 回收VelocityTracker對象。
- */
- private void recycleVelocityTracker() {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- class ScrollTask extends AsyncTask<Integer, Integer, Integer> {
- @Override
- protected Integer doInBackground(Integer... speed) {
- int leftMargin = leftLayoutParams.leftMargin;
- // 根據傳入的速度來滾動界面,當滾動到達左邊界或右邊界時,跳出循環。
- while (true) {
- leftMargin = leftMargin + speed[0];
- if (leftMargin > rightEdge) {
- leftMargin = rightEdge;
- break;
- }
- if (leftMargin < leftEdge) {
- leftMargin = leftEdge;
- break;
- }
- publishProgress(leftMargin);
- // 為了要有滾動效果產生,每次循環使線程睡眠20毫秒,這樣肉眼才能夠看到滾動動畫。
- sleep(20);
- }
- if (speed[0] > 0) {
- isLeftLayoutVisible = true;
- } else {
- isLeftLayoutVisible = false;
- }
- return leftMargin;
- }
- @Override
- protected void onProgressUpdate(Integer... leftMargin) {
- leftLayoutParams.leftMargin = leftMargin[0];
- leftLayout.setLayoutParams(leftLayoutParams);
- }
- @Override
- protected void onPostExecute(Integer leftMargin) {
- leftLayoutParams.leftMargin = leftMargin;
- leftLayout.setLayoutParams(leftLayoutParams);
- }
- }
- /**
- * 使當前線程睡眠指定的毫秒數。
- *
- * @param millis
- * 指定當前線程睡眠多久,以毫秒為單位
- */
- private void sleep(long millis) {
- try {
- Thread.sleep(millis);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
看到這裡,我相信大家一定會覺得這些代碼非常熟悉。沒錯,基本上這些代碼和之前那篇文章的代碼大同小異,只不過以前這些代碼是寫在Activity裡的,而現在我們移動到了自定義的View當中。
接著我來說明一下和以前不同的部分。我們可以看到,這裡將onLayout方法進行了重寫,使用getChildAt(0)獲取到的布局作為左邊布局,使用getChildAt(1)獲取到的布局作為右邊布局。並將左邊布局的寬度重定義為屏幕寬度減去leftLayoutPadding,將右側布局的寬度重定義為屏幕寬度。然後讓左邊布局偏移出屏幕,這樣能看到的就只有右邊布局了。因此在這裡我們也可以看出,使用SlidingLayout這個布局的前提條件,必須為這個布局提供兩個子元素,第一個元素會作為左邊布局偏移出屏幕,第二個元素會作為右邊布局顯示在屏幕上。
然後我們看一下setScrollEvent方法,這個方法接收一個View作為參數,然後為這個View綁定了一個touch事件。這是什麼意思呢?讓我們來想象一個場景,如果右側布局是一個LinearLayout,我可以通過監聽LinearLayout上的touch事件來控制左側布局的顯示和隱藏。但是如果右側布局的LinearLayout裡面加入了一個ListView,而這個ListView又充滿了整個LinearLayout,這個時候LinearLayout將不可能再被touch到了,這個時候我們就需要將touch事件注冊到ListView上。setScrollEvent方法也就是提供了一個注冊接口,touch事件將會注冊到傳入的View上。
最後還有一個陌生的方法,isBindBasicLayout。這個方法就是判斷了一下注冊touch事件的View是不是四個基本布局之一,如果是就返回true,否則返回false。這個方法在整個SlidingLayout中起著非常重要的作用,主要用於控制onTouch事件是返回true還是false,這將影響到布局當中的View的功能是否可用。由於裡面牽扯到了Android的事件轉發機制,內容比較多,就不在這裡詳細解釋了,我會考慮以後專門寫一篇文章來介紹Android的事件機制。這裡就先簡單記住如果是基本布局就返回true,否則就返回false。
好了,我們的SlidingLayout寫完了,接下來就是見證奇跡的時刻,讓我們一起看看如何一分鐘在Activity中引入滑動菜單功能。
創建或打開layout目錄下的activity_main.xml文件,加入如下代碼:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="horizontal"
- tools:context=".MainActivity" >
- <!-- 使用自定義的側滑布局,orientation必須為水平方向 -->
- <com.example.slide.SlidingLayout
- android:id="@+id/slidingLayout"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="horizontal" >
- <!--
- 側滑布局的根節點下,有且只能有兩個子元素,這兩個子元素必須是四種基本布局之一,
- 即LinearLayout, RelativeLayout, FrameLayout或TableLayout。
- 第一個子元素將做為左側布局,初始化後被隱藏。第二個子元素將做為右側布局,
- 也就是當前Activity的主布局,將主要的數據放在裡面。
- -->
- <RelativeLayout
- android:id="@+id/menu"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:background="#00ccff" >
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:text="This is menu"
- android:textColor="#000000"
- android:textSize="28sp" />
- </RelativeLayout>
- <LinearLayout
- android:id="@+id/content"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical" >
- <Button
- android:id="@+id/menuButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Menu" />
- <ListView
- android:id="@+id/contentList"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- </ListView>
- </LinearLayout>
- </com.example.slide.SlidingLayout>
- </LinearLayout>
我們可以看到,在根布局的下面,我們引入了自定義布局com.example.slide.SlidingLayout,然後在它裡面加入了兩個子元素,一個RelativeLayout和一個LinearLayout。RelativeLayout中比較簡單,就加入了一個TextView。LinearLayout裡面我們加入了一個按鈕和一個ListView。
然後創建或打開MainActivity作為程序的主Activity,加入代碼:
- public class MainActivity extends Activity {
- /**
- * 側滑布局對象,用於通過手指滑動將左側的菜單布局進行顯示或隱藏。
- */
- private SlidingLayout slidingLayout;
- /**
- * menu按鈕,點擊按鈕展示左側布局,再點擊一次隱藏左側布局。
- */
- private Button menuButton;
- /**
- * 放在content布局中的ListView。
- */
- private ListView contentListView;
- /**
- * 作用於contentListView的適配器。
- */
- private ArrayAdapter<String> contentListAdapter;
- /**
- * 用於填充contentListAdapter的數據源。
- */
- private String[] contentItems = { "Content Item 1", "Content Item 2", "Content Item 3",
- "Content Item 4", "Content Item 5", "Content Item 6", "Content Item 7",
- "Content Item 8", "Content Item 9", "Content Item 10", "Content Item 11",
- "Content Item 12", "Content Item 13", "Content Item 14", "Content Item 15",
- "Content Item 16" };
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- slidingLayout = (SlidingLayout) findViewById(R.id.slidingLayout);
- menuButton = (Button) findViewById(R.id.menuButton);
- contentListView = (ListView) findViewById(R.id.contentList);
- contentListAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
- contentItems);
- contentListView.setAdapter(contentListAdapter);
- // 將監聽滑動事件綁定在contentListView上
- slidingLayout.setScrollEvent(contentListView);
- menuButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- // 實現點擊一下menu展示左側布局,再點擊一下隱藏左側布局的功能
- if (slidingLayout.isLeftLayoutVisible()) {
- slidingLayout.scrollToRightLayout();
- } else {
- slidingLayout.scrollToLeftLayout();
- }
- }
- });
- }
- }
上述代碼重點是調用SlidingLayout的setScrollEvent方法,為ListView注冊touch事件。同時給按鈕添加了一個點擊事件,實現了點擊一下顯示左邊布局,再點擊一下隱藏左邊布局的功能。
最後還是老規矩,給出AndroidManifest.xml的代碼:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.example.slide"
- android:versionCode="1"
- android:versionName="1.0" >
- <uses-sdk
- android:minSdkVersion="8"
- android:targetSdkVersion="8" />
- <application
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@android:style/Theme.NoTitleBar" >
- <activity
- android:name="com.example.slide.MainActivity"
- android:label="@string/app_name" >
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
- </manifest>
好了,現在讓我們運行一下吧。首先是程序打開的時候,顯示的是右邊布局。用手指在界面上向右滑動,可以看到左邊布局出現。
而當左邊布局完全顯示的時候,效果圖如下:
除此之外,點擊Menu按鈕也可以控制左邊布局的顯示和隱藏,大家可以自己試一下。
使用自定義布局的話,就可以用簡單的方式在任意Activity中加入滑動菜單功能,即使你有再多的Activity也不用怕了,一分鐘引入滑動菜單妥妥的。
再總結一下吧,向Activity中加入滑動菜單功能只需要兩步:
1. 在Acitivty的layout中引入我們自定義的布局,並且給這個布局要加入兩個直接子元素。
2. 在Activity中通過setScrollEvent方法,給一個View注冊touch事件。
由於這段文章寫的比較早了,那時寫的滑動菜單還存在著不少問題,我之後又將上面的代碼做了不少改動,編寫了一個修正版的滑動菜單,這個版本主要修正了以下內容:
1.將滑動方式改成了覆蓋型。
2.ListView上下滾動時不會輕易滑出菜單。
3.正在滑動時屏蔽掉內容布局上的事件。
4.當菜單布局展示時,點擊一下右側的內容布局,可以將菜單隱藏。
5.修復剛打開程序時,菜單可能會短暫顯示一下,然後瞬間消失的bug。
源碼下載,請點擊這裡
Android應用程序可以在許多不同地區的許多設備上運行。為了使應用程序更具交互性,應用程序應該處理以適合應用程序將要使用的語言環境方面的文字,數字,文件等。在本章中,我
SAX是一個解析速度快並且占用內存少的xml解析器,非常適合用於Android等移動設備。 SAX解析XML文件采用的是事件驅動,也就是說,它並不需要解析完整個文
本人工作有一個月多了。對於android很多東西,都有了新的了解或者說真正的掌握。為了讓更多的像我這樣的小白少走彎路,所以我會堅持將我在工作中遇到的一些比較令我印
雜語:看了很多程序猿都有寫博客的習慣,看來我也得練練,不管寫的好不好了,學到點什麼體會就寫寫吧。 內容解說:這幾天開始學游戲地圖制作,今天小小的總結一下Canva