突發奇想,想自己實現RecyclerView中的滑動菜單控件。看了幾篇大神的文章,有繼承自ViewGroup實現的,有繼承RecyclerView實現的,等等...但是都不是太符合我的預期,我希望的是一個簡單的,並且能很快使用的舊項目中的。因此,在看了幾篇文章後,決定自己來嘗試着寫一個可以很快使舊項目也能新增滑動菜單的控件。老規矩,先上效果圖。
效果圖:
效果很常見,沒有炫酷的動畫,因爲要考慮能快速兼容老項目的原因。只是常規的展開和收起,根據拖動的距離來自動判斷是該收起還是該展開。
思路分析:
其實剛開始我是針對RecyclerView的觸摸事件進行攔截,然後自己去寫邏輯來實現滑動效果的。但是技術不佳,實現的效果太差了,而且始終不知道該如何將菜單佈局加入進去還不破壞原有的item佈局。後來看到一篇文章,使用了HorizontalScrollView,採用了他的思路,發現真的非常方便。鏈接:RecyclerView 側滑刪除菜單 最簡版 沒有之一。
因此,我的這個控件也是採用繼承HorizontalScrollView來實現,主要分爲一個用於包裹Item佈局的FrameLayout和一個用於包裹側滑菜單佈局的FrameLayou,它們都被一個水平的LinearLayout包裹。這個LinearLayout最終被HorizontalScrollView所包裹着。
代碼實現:
在/res/values下新建一個attr.xml文件,用於自定義屬性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ScrollMenuLayout">
<attr name="itemLayout" format="reference"/>
<attr name="rightMenuLayout" format="reference"/>
</declare-styleable>
</resources>
然後新建一個Java類,取名ScrollMenuLayout並繼承HorizontalScrollView,實現邏輯我將在代碼片段中用註釋的形式進行說明,這樣子更容易理解:
public class ScrollMenuLayout extends HorizontalScrollView {
private static final String TAG = "MenuItem";
private FrameLayout container_item;//用於包裹Item佈局的容器
private FrameLayout container_menu_right;//用於包裹側滑菜單佈局的容器
private float lastX;//記錄上一次觸摸的x軸的值,也就是橫向的位置
private float moveX;//累計自手指按下→移動→擡起,這個過程中移動的距離
public ScrollMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setHorizontalScrollBarEnabled(false);//隱藏滾動條
//獲取xml文件中的屬性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ScrollMenuLayout);
//new一個水平的線性佈局容器,用來水平放置兩個FrameLayout
LinearLayout linearLayout = new LinearLayout(context);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
//將兩個容器實例化
container_item = new FrameLayout(context);
container_menu_right = new FrameLayout(context);
//如果在xml文件中設置了佈局id,則直接加載
int item_layout_id = array.getResourceId(R.styleable.ScrollMenuLayout_itemLayout, -1);
if (item_layout_id != -1) {
//將Item佈局文件加載到FrameLayout中,注意使用LayoutInflater時,第一個參數爲佈局文件id,第二個參數爲佈局容器
//由於我使用了addView的方式,所以第二個參數要填Null,否則會出現Item已經有父容器的錯誤
container_item.addView(LayoutInflater.from(context).inflate(item_layout_id, null));
}
int menu_layout_right_id = array.getResourceId(R.styleable.ScrollMenuLayout_rightMenuLayout, -1);
if (menu_layout_right_id != -1) {
//同上的邏輯
container_menu_right.addView(LayoutInflater.from(context).inflate(menu_layout_right_id, null));
}
//組裝
linearLayout.addView(container_item);
linearLayout.addView(container_menu_right);
addView(linearLayout);
array.recycle();//回收屬性數組
}
@Override
protected void onDraw(Canvas canvas) {
//將Item的寬度設爲父容器的寬度,用於將側滑菜單頂出視野
//放在onDraw執行是爲了保證能獲取到父容器的寬度,這裏的父容器指的就是在Adapter中
//onCreateViewHolder方法的第二個參數ViewGroup
ViewGroup.LayoutParams layoutParams = container_item.getLayoutParams();
layoutParams.width = ((ViewGroup) getParent()).getWidth();
container_item.setLayoutParams(layoutParams);
super.onDraw(canvas);
}
/**
* 設置item佈局
*
* @param v item佈局view
*/
public void setItemView(View v) {
container_item.removeAllViews();
container_item.addView(v);
}
/**
* 獲取item佈局view
* 方便去做各種監聽等等
*
* @return Null or View
*/
public View getItemView() {
if (container_item.getChildCount() > 0) {
return container_item.getChildAt(0);
}
return null;
}
/**
* 設置右邊的菜單
*
* @param v 右邊菜單佈局View
*/
public void setRightMenuView(View v) {
container_menu_right.removeAllViews();
container_menu_right.addView(v);
}
/**
* 獲取右邊的菜單佈局View
*
* @return Null or View
*/
public View getRightMenuView() {
if (container_menu_right.getChildCount() > 0) {
return container_menu_right.getChildAt(0);
}
return null;
}
/**
* 展開右邊菜單
*/
public void expandRightMenu() {
arrowScroll(FOCUS_RIGHT);
}
/**
* 收起右邊菜單
*/
public void closeRightMenu() {
arrowScroll(FOCUS_LEFT);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = ev.getX();
break;
case MotionEvent.ACTION_MOVE:
//記錄移動距離
moveX += ev.getX() - lastX;
lastX = ev.getX();
Log.d(TAG, "menu width = " + container_menu_right.getWidth() + " moveX = " + moveX);
break;
case MotionEvent.ACTION_UP:
if (moveX > 0) {
//意圖:收起,手指從左向右滑動
if (Math.abs(moveX) >= container_menu_right.getWidth() / 2) {
//滑動距離大於一半,收起
closeRightMenu();
} else {
//展開
expandRightMenu();
}
} else if (moveX < 0) {
//意圖展開,手指從右向左滑動
if (Math.abs(moveX) >= container_menu_right.getWidth() / 2) {
//展開
expandRightMenu();
} else {
//收起
closeRightMenu();
}
}
moveX = 0;//重置
return true;//消費該次事件,不再傳遞,解決滑動衝突
}
return super.onTouchEvent(ev);
}
}
可以看到,其實主要的實現就是將Item佈局的寬度設置爲整個HorizontalScrollView父容器的寬度,然後就可以剛好將側滑菜單的佈局給頂出視野範圍,我們再重寫HorizontalScrollView的滑動邏輯, 當滑動的距離大於側滑菜單佈局的寬度的一半的時候,就自動的將側滑菜單彈出或者收起。
其實整個控件的邏輯及其簡單,只要注意下在onTouch中,event.getX()的值爲負數的時候表示手指正在從右向左滑動,反之則爲從左向右滑動。
結束語:
到此,整個控件的實現就完成了,由於採用了將Item佈局和側滑菜單佈局分開的方式,所以整個控件能很快的替換老項目的佈局,並且不需要修改太多的東西就能完成側滑的功能。當然,如果需要一些比較炫酷的動畫,這個就需要大家自己去實現了,我後期也會考慮試試再新增動畫的功能。
項目我已經開源到了GitHub上,並且附帶了demo,歡迎大家查閱。GitHub鏈接