編輯: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();
}
}
}
Android開發入門(九)用戶界面 9.3 注冊事件監聽器
當用戶與視圖views進行交互的時候,views也會觸發事件。舉個例子,當用戶點擊了一個按鈕,你需要為 這個事件服務,只有這樣,才能去執行某些適當的行為。如果想這麼做的話
Android版Chrome支持更快的安全加密算法
谷歌最近通過控制浏覽器及其訪問的站點來加速Android平台安全網頁的浏覽——谷歌anti-abuse研究團隊主管Elie Bursztein在本
Android測試教程(3):測試項目
Android的編譯和測試工具需要測試項目組織符合預訂的結構:分別為Test case 類,Test case 包以及測試項目。JUnit 為Android的測試的基礎,
Android UI設計與開發教程 引導界面(三)仿微信引導界面以及動畫效果
這篇要實現的是一個仿微信的動畫效 果,雖然這種效果的實現在網上到處都有,但是我還是想站在中低端開發者的角度去告訴大家是如何實現的, 當然實現的方式有很多,我也只是列出