編輯:Android開發教程
記得在很早之前,我寫了一篇關於Android滑動菜單的文章,其中有一個朋友在評論中留言,希望我可以 幫他將這個滑動菜單改成雙向滑動的方式。當時也沒想花太多時間,簡單修改了一下就發給了他,結果沒想 到後來卻有一大批的朋友都來問我要這份雙向滑動菜單的代碼。由於這份代碼寫得很不用心,我發了部分朋 友之後實在不忍心繼續發下去了,於是決定專門寫一篇文章來介紹更好的Android雙向滑動菜單的實現方法。
在開始動手之前先來講一下實現原理,在一個Activity的布局中需要有三部分,一個是左側菜單的布 局,一個是右側菜單的布局,一個是內容布局。左側菜單居屏幕左邊緣對齊,右側菜單居屏幕右邊緣對齊, 然後內容布局占滿整個屏幕,並壓在了左側菜單和右側菜單的上面。當用戶手指向右滑動時,將右側菜單隱 藏,左側菜單顯示,然後通過偏移內容布局的位置,就可以讓左側菜單展現出來。同樣的道理,當用戶手指 向左滑動時,將左側菜單隱藏,右側菜單顯示,也是通過偏移內容布局的位置,就可以讓右側菜單展現出來 。原理示意圖所下所示:
介紹完了原理,我們就開始動 手實現吧。新建一個Android項目,項目名就叫做BidirSlidingLayout。然後新建我們最主要的 BidirSlidingLayout類,這個類就是實現雙向滑動菜單功能的核心類,代碼如下所示:
public class BidirSlidingLayout extends RelativeLayout implements OnTouchListener { /** * 滾動顯示和隱藏左側布局時,手指滑動需要達到的速度。 */ public static final int SNAP_VELOCITY = 200; /** * 滑動狀態的一種,表示未進行任何滑動。 */ public static final int DO_NOTHING = 0; /** * 滑動狀態的一種,表示正在滑出左側菜單。 */ public static final int SHOW_LEFT_MENU = 1; /** * 滑動狀態的一種,表示正在滑出右側菜單。 */ public static final int SHOW_RIGHT_MENU = 2; /** * 滑動狀態的一種,表示正在隱藏左側菜單。 */ public static final int HIDE_LEFT_MENU = 3; /** * 滑動狀態的一種,表示正在隱藏右側菜單。 */ public static final int HIDE_RIGHT_MENU = 4; /** * 記錄當前的滑動狀態 */ private int slideState; /** * 屏幕寬度值。 */ private int screenWidth; /** * 在被判定為滾動之前用戶手指可以移動的最大值。 */ private int touchSlop; /** * 記錄手指按下時的橫坐標。 */ private float xDown; /** * 記錄手指按下時的縱坐標。 */ private float yDown; /** * 記錄手指移動時的橫坐標。 */ private float xMove; /** * 記錄手指移動時的縱坐標。 */ private float yMove; /** * 記錄手機抬起時的橫坐標。 */ private float xUp; /** * 左側菜單當前是顯示還是隱藏。只有完全顯示或隱藏時才會更改此值,滑動過程中此值無效。 */ private boolean isLeftMenuVisible; /** * 右側菜單當前是顯示還是隱藏。只有完全顯示或隱藏時才會更改此值,滑動過程中此值無效。 */ private boolean isRightMenuVisible; /** * 是否正在滑動。 */ private boolean isSliding; /** * 左側菜單布局對象。 */ private View leftMenuLayout; /** * 右側菜單布局對象。 */ private View rightMenuLayout; /** * 內容布局對象。 */ private View contentLayout; /** * 用於監聽滑動事件的View。 */ private View mBindView; /** * 左側菜單布局的參數。 */ private MarginLayoutParams leftMenuLayoutParams; /** * 右側菜單布局的參數。 */ private MarginLayoutParams rightMenuLayoutParams; /** * 內容布局的參數。 */ private RelativeLayout.LayoutParams contentLayoutParams; /** * 用於計算手指滑動的速度。 */ private VelocityTracker mVelocityTracker; /** * 重寫BidirSlidingLayout的構造函數,其中獲取了屏幕的寬度和touchSlop的值。 * * @param context * @param attrs */ public BidirSlidingLayout(Context context, AttributeSet attrs) { super(context, attrs); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); screenWidth = wm.getDefaultDisplay().getWidth(); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } /** * 綁定監聽滑動事件的View。 * * @param bindView * 需要綁定的View對象。 */ public void setScrollEvent(View bindView) { mBindView = bindView; mBindView.setOnTouchListener(this); } /** * 將界面滾動到左側菜單界面,滾動速度設定為-30. */ public void scrollToLeftMenu() { new LeftMenuScrollTask().execute(-30); } /** * 將界面滾動到右側菜單界面,滾動速度設定為-30. */ public void scrollToRightMenu() { new RightMenuScrollTask().execute(-30); } /** * 將界面從左側菜單滾動到內容界面,滾動速度設定為30. */ public void scrollToContentFromLeftMenu() { new LeftMenuScrollTask().execute(30); } /** * 將界面從右側菜單滾動到內容界面,滾動速度設定為30. */ public void scrollToContentFromRightMenu() { new RightMenuScrollTask().execute(30); } /** * 左側菜單是否完全顯示出來,滑動過程中此值無效。 * * @return 左側菜單完全顯示返回true,否則返回false。 */ public boolean isLeftLayoutVisible() { return isLeftMenuVisible; } /** * 右側菜單是否完全顯示出來,滑動過程中此值無效。 * * @return 右側菜單完全顯示返回true,否則返回false。 */ public boolean isRightLayoutVisible() { return isRightMenuVisible; } /** * 在onLayout中重新設定左側菜單、右側菜單、以及內容布局的參數。 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed) { // 獲取左側菜單布局對象 leftMenuLayout = getChildAt(0); leftMenuLayoutParams = (MarginLayoutParams) leftMenuLayout.getLayoutParams(); // 獲取右側菜單布局對象 rightMenuLayout = getChildAt(1); rightMenuLayoutParams = (MarginLayoutParams) rightMenuLayout.getLayoutParams(); // 獲取內容布局對象 contentLayout = getChildAt(2); contentLayoutParams = (RelativeLayout.LayoutParams) contentLayout.getLayoutParams (); contentLayoutParams.width = screenWidth; contentLayout.setLayoutParams(contentLayoutParams); } } @Override public boolean onTouch(View v, MotionEvent event) { createVelocityTracker(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 手指按下時,記錄按下時的坐標 xDown = event.getRawX(); yDown = event.getRawY(); // 將滑動狀態初始化為DO_NOTHING slideState = DO_NOTHING; break; case MotionEvent.ACTION_MOVE: xMove = event.getRawX(); yMove = event.getRawY(); // 手指移動時,對比按下時的坐標,計算出移動的距離。 int moveDistanceX = (int) (xMove - xDown); int moveDistanceY = (int) (yMove - yDown); // 檢查當前的滑動狀態 checkSlideState(moveDistanceX, moveDistanceY); // 根據當前滑動狀態決定如何偏移內容布局 switch (slideState) { case SHOW_LEFT_MENU: contentLayoutParams.rightMargin = -moveDistanceX; checkLeftMenuBorder(); contentLayout.setLayoutParams(contentLayoutParams); break; case HIDE_LEFT_MENU: contentLayoutParams.rightMargin = -leftMenuLayoutParams.width - moveDistanceX; checkLeftMenuBorder(); contentLayout.setLayoutParams(contentLayoutParams); case SHOW_RIGHT_MENU: contentLayoutParams.leftMargin = moveDistanceX; checkRightMenuBorder(); contentLayout.setLayoutParams(contentLayoutParams); break; case HIDE_RIGHT_MENU: contentLayoutParams.leftMargin = -rightMenuLayoutParams.width + moveDistanceX; checkRightMenuBorder(); contentLayout.setLayoutParams(contentLayoutParams); default: break; } break; case MotionEvent.ACTION_UP: xUp = event.getRawX(); int upDistanceX = (int) (xUp - xDown); if (isSliding) { // 手指抬起時,進行判斷當前手勢的意圖 switch (slideState) { case SHOW_LEFT_MENU: if (shouldScrollToLeftMenu()) { scrollToLeftMenu(); } else { scrollToContentFromLeftMenu(); } break; case HIDE_LEFT_MENU: if (shouldScrollToContentFromLeftMenu()) { scrollToContentFromLeftMenu(); } else { scrollToLeftMenu(); } break; case SHOW_RIGHT_MENU: if (shouldScrollToRightMenu()) { scrollToRightMenu(); } else { scrollToContentFromRightMenu(); } break; case HIDE_RIGHT_MENU: if (shouldScrollToContentFromRightMenu()) { scrollToContentFromRightMenu(); } else { scrollToRightMenu(); } break; default: break; } } else if (upDistanceX < touchSlop && isLeftMenuVisible) { // 當左側菜單顯示時,如果用戶點擊一下內容部分,則直接滾動到內容界面 scrollToContentFromLeftMenu(); } else if (upDistanceX < touchSlop && isRightMenuVisible) { // 當右側菜單顯示時,如果用戶點擊一下內容部分,則直接滾動到內容界面 scrollToContentFromRightMenu(); } recycleVelocityTracker(); break; } if (v.isEnabled()) { if (isSliding) { // 正在滑動時讓控件得不到焦點 unFocusBindView(); return true; } if (isLeftMenuVisible || isRightMenuVisible) { // 當左側或右側布局顯示時,將綁定控件的事件屏蔽掉 return true; } return false; } return true; } /** * 根據手指移動的距離,判斷當前用戶的滑動意圖,然後給slideState賦值成相應的滑動狀態值。 * * @param moveDistanceX * 橫向移動的距離 * @param moveDistanceY * 縱向移動的距離 */ private void checkSlideState(int moveDistanceX, int moveDistanceY) { if (isLeftMenuVisible) { if (!isSliding && Math.abs(moveDistanceX) >= touchSlop && moveDistanceX < 0) { isSliding = true; slideState = HIDE_LEFT_MENU; } } else if (isRightMenuVisible) { if (!isSliding && Math.abs(moveDistanceX) >= touchSlop && moveDistanceX > 0) { isSliding = true; slideState = HIDE_RIGHT_MENU; } } else { if (!isSliding && Math.abs(moveDistanceX) >= touchSlop && moveDistanceX > 0 && Math.abs(moveDistanceY) < touchSlop) { isSliding = true; slideState = SHOW_LEFT_MENU; contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0); contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); contentLayout.setLayoutParams(contentLayoutParams); // 如果用戶想要滑動左側菜單,將左側菜單顯示,右側菜單隱藏 leftMenuLayout.setVisibility(View.VISIBLE); rightMenuLayout.setVisibility(View.GONE); } else if (!isSliding && Math.abs(moveDistanceX) >= touchSlop && moveDistanceX < 0 && Math.abs(moveDistanceY) < touchSlop) { isSliding = true; slideState = SHOW_RIGHT_MENU; contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0); contentLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); contentLayout.setLayoutParams(contentLayoutParams); // 如果用戶想要滑動右側菜單,將右側菜單顯示,左側菜單隱藏 rightMenuLayout.setVisibility(View.VISIBLE); leftMenuLayout.setVisibility(View.GONE); } } } /** * 在滑動過程中檢查左側菜單的邊界值,防止綁定布局滑出屏幕。 */ private void checkLeftMenuBorder() { if (contentLayoutParams.rightMargin > 0) { contentLayoutParams.rightMargin = 0; } else if (contentLayoutParams.rightMargin < -leftMenuLayoutParams.width) { contentLayoutParams.rightMargin = -leftMenuLayoutParams.width; } } /** * 在滑動過程中檢查右側菜單的邊界值,防止綁定布局滑出屏幕。 */ private void checkRightMenuBorder() { if (contentLayoutParams.leftMargin > 0) { contentLayoutParams.leftMargin = 0; } else if (contentLayoutParams.leftMargin < -rightMenuLayoutParams.width) { contentLayoutParams.leftMargin = -rightMenuLayoutParams.width; } } /** * 判斷是否應該滾動將左側菜單展示出來。如果手指移動距離大於左側菜單寬度的1/2,或者手指移動 速度大於SNAP_VELOCITY, * 就認為應該滾動將左側菜單展示出來。 * * @return 如果應該將左側菜單展示出來返回true,否則返回false。 */ private boolean shouldScrollToLeftMenu() { return xUp - xDown > leftMenuLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY; } /** * 判斷是否應該滾動將右側菜單展示出來。如果手指移動距離大於右側菜單寬度的1/2,或者手指移動 速度大於SNAP_VELOCITY, * 就認為應該滾動將右側菜單展示出來。 * * @return 如果應該將右側菜單展示出來返回true,否則返回false。 */ private boolean shouldScrollToRightMenu() { return xDown - xUp > rightMenuLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY; } /** * 判斷是否應該從左側菜單滾動到內容布局,如果手指移動距離大於左側菜單寬度的1/2,或者手指移 動速度大於SNAP_VELOCITY, * 就認為應該從左側菜單滾動到內容布局。 * * @return 如果應該從左側菜單滾動到內容布局返回true,否則返回false。 */ private boolean shouldScrollToContentFromLeftMenu() { return xDown - xUp > leftMenuLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY; } /** * 判斷是否應該從右側菜單滾動到內容布局,如果手指移動距離大於右側菜單寬度的1/2,或者手指移 動速度大於SNAP_VELOCITY, * 就認為應該從右側菜單滾動到內容布局。 * * @return 如果應該從右側菜單滾動到內容布局返回true,否則返回false。 */ private boolean shouldScrollToContentFromRightMenu() { return xUp - xDown > rightMenuLayoutParams.width / 2 || getScrollVelocity() > SNAP_VELOCITY; } /** * 創建VelocityTracker對象,並將觸摸事件加入到VelocityTracker當中。 * * @param event * 右側布局監聽控件的滑動事件 */ private void createVelocityTracker(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } /** * 獲取手指在綁定布局上的滑動速度。 * * @return 滑動速度,以每秒鐘移動了多少像素值為單位。 */ private int getScrollVelocity() { mVelocityTracker.computeCurrentVelocity(1000); int velocity = (int) mVelocityTracker.getXVelocity(); return Math.abs(velocity); } /** * 回收VelocityTracker對象。 */ private void recycleVelocityTracker() { mVelocityTracker.recycle(); mVelocityTracker = null; } /** * 使用可以獲得焦點的控件在滑動的時候失去焦點。 */ private void unFocusBindView() { if (mBindView != null) { mBindView.setPressed(false); mBindView.setFocusable(false); mBindView.setFocusableInTouchMode(false); } } class LeftMenuScrollTask extends AsyncTask<Integer, Integer, Integer> { @Override protected Integer doInBackground(Integer... speed) { int rightMargin = contentLayoutParams.rightMargin; // 根據傳入的速度來滾動界面,當滾動到達邊界值時,跳出循環。 while (true) { rightMargin = rightMargin + speed[0]; if (rightMargin < -leftMenuLayoutParams.width) { rightMargin = -leftMenuLayoutParams.width; break; } if (rightMargin > 0) { rightMargin = 0; break; } publishProgress(rightMargin); // 為了要有滾動效果產生,每次循環使線程睡眠一段時間,這樣肉眼才能夠看到滾動動畫 。 sleep(15); } if (speed[0] > 0) { isLeftMenuVisible = false; } else { isLeftMenuVisible = true; } isSliding = false; return rightMargin; } @Override protected void onProgressUpdate(Integer... rightMargin) { contentLayoutParams.rightMargin = rightMargin[0]; contentLayout.setLayoutParams(contentLayoutParams); unFocusBindView(); } @Override protected void onPostExecute(Integer rightMargin) { contentLayoutParams.rightMargin = rightMargin; contentLayout.setLayoutParams(contentLayoutParams); } } class RightMenuScrollTask extends AsyncTask<Integer, Integer, Integer> { @Override protected Integer doInBackground(Integer... speed) { int leftMargin = contentLayoutParams.leftMargin; // 根據傳入的速度來滾動界面,當滾動到達邊界值時,跳出循環。 while (true) { leftMargin = leftMargin + speed[0]; if (leftMargin < -rightMenuLayoutParams.width) { leftMargin = -rightMenuLayoutParams.width; break; } if (leftMargin > 0) { leftMargin = 0; break; } publishProgress(leftMargin); // 為了要有滾動效果產生,每次循環使線程睡眠一段時間,這樣肉眼才能夠看到滾動動畫 。 sleep(15); } if (speed[0] > 0) { isRightMenuVisible = false; } else { isRightMenuVisible = true; } isSliding = false; return leftMargin; } @Override protected void onProgressUpdate(Integer... leftMargin) { contentLayoutParams.leftMargin = leftMargin[0]; contentLayout.setLayoutParams(contentLayoutParams); unFocusBindView(); } @Override protected void onPostExecute(Integer leftMargin) { contentLayoutParams.leftMargin = leftMargin; contentLayout.setLayoutParams(contentLayoutParams); } } /** * 使當前線程睡眠指定的毫秒數。 * * @param millis * 指定當前線程睡眠多久,以毫秒為單位 */ private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } }
當用戶與視圖views進行交互的時候,views也會觸發事件。舉個例子,當用戶點擊了一個按鈕,你需要為 這個事件服務,只有這樣,才能去執行某些適當的行為。如果想這麼做的話
谷歌最近通過控制浏覽器及其訪問的站點來加速Android平台安全網頁的浏覽——谷歌anti-abuse研究團隊主管Elie Bursztein在本
Android的編譯和測試工具需要測試項目組織符合預訂的結構:分別為Test case 類,Test case 包以及測試項目。JUnit 為Android的測試的基礎,
這篇要實現的是一個仿微信的動畫效 果,雖然這種效果的實現在網上到處都有,但是我還是想站在中低端開發者的角度去告訴大家是如何實現的, 當然實現的方式有很多,我也只是列出