ListView 側(cè)滑菜單的實(shí)現(xiàn) -- 大道至簡(jiǎn)的SwipeMenuLayout

SwipeMenuLayout

View for listView item

一 介紹

 ListView 通常用來(lái)展示多個(gè)個(gè)體复亏,比如QQ 微信中的聯(lián)系人列表。一個(gè)比較常見(jiàn)的功能是側(cè)滑刪除臊泰。這個(gè)功能屬于比較常見(jiàn)的一個(gè)菜單,網(wǎng)絡(luò)上也有很多實(shí)現(xiàn)蚜枢。

1 scroller 方式缸逃。

最常見(jiàn)的一個(gè)實(shí)現(xiàn)是ListView 的Item View 為一個(gè)LinerLayout, 菜單在LinerLayout的最右端超出屏幕的位置针饥,當(dāng)手指滑動(dòng)的時(shí)候,通過(guò)scrollTo 的方法在ListView 中控制Item View 的滑動(dòng)需频,使菜單滑動(dòng)出來(lái)丁眼。但是在IOS 上菜單是隱藏在Item View 的下面,層疊式的昭殉,當(dāng)滑動(dòng)的時(shí)候不是拉出來(lái)的方式苞七,而是顯示出來(lái)。

2.NineOldAndroids

在屬性動(dòng)畫(huà)沒(méi)有加入android的遠(yuǎn)古時(shí)代挪丢,github 上有一個(gè)NineOldAndroids項(xiàng)目蹂风,有人通過(guò)這個(gè)實(shí)現(xiàn)一個(gè)和IOS接近,其原理是FrameLayout乾蓬, context 為顯示的內(nèi)容惠啄,menu嵌套在context下面,屬性動(dòng)畫(huà)的方式移動(dòng)context任内。

3.SwipeMenuListView

在github 上有一個(gè) https://github.com/baoyongzhang/SwipeMenuListView 實(shí)現(xiàn)效果和1 類似撵渡,但是View 移動(dòng)采用layout 方式,我修改了下 https://github.com/louiewh/SwipeMenuListView   實(shí)現(xiàn)效果和IOS 一樣死嗦。但是總覺(jué)這幾種方式都不太完美趋距,要么效果打了折扣,要么代碼量太大越走,方式復(fù)雜棚品,通常需要重寫(xiě)ListView 和Adapter。

二 原理

 下面一個(gè)SwipeMenuLayout 大概不到300行的代碼廊敌,完美實(shí)現(xiàn)ListView 的側(cè)滑菜單铜跑。原理是繼承FrameLayout, 作為L(zhǎng)istView 的Item View 骡澈。在View中層疊兩層锅纺,上層context View 為要顯示的內(nèi)容,下面menu View 為菜單肋殴。重寫(xiě)SwipeMenuLayout的OnTouchEvent ,在OnTouchEvent 中控制context 的移動(dòng)囤锉。如圖:
image
image

1. init View.

定義ListView 的Item View,如果使用左菜單:

  1. 左菜單的ID為:swipe_left_menu
  2. 右菜單ID:swipe_right_menu
  3. context 菜單ID:swipe_context

這個(gè)是默認(rèn)ID护锤,這樣在SwipeMenuLayout會(huì)自動(dòng)找到菜單View ID :

    public SwipeMenuLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttrs(attrs);
        initUI();
    }

    private void initUI() {
        mScroller = ScrollerCompat.create(getContext());

        ViewConfiguration config = ViewConfiguration.get(getContext());
        mTouchSlop = config.getScaledTouchSlop();

        mLeftMenuViewId  = getContext().getResources().getIdentifier(LEFTMENUVIEW, "id", getContext().getPackageName());
        mRightMenuViewId = getContext().getResources().getIdentifier(RIGHTMENUVIEW, "id", getContext().getPackageName());
        mContextViewId   = getContext().getResources().getIdentifier(CONTEXTVIEW, "id", getContext().getPackageName());

        if (mLeftMenuViewId == 0 || mRightMenuViewId == 0 || mContextViewId == 0) {
            throw new RuntimeException(String.format("initUI Exception" ));
        }
    }

重寫(xiě) onMeasure方法官地, 在onMeasure 方法中調(diào)用findViewByID, 為什么在onMeasure 而不是在構(gòu)造函數(shù)中烙懦,因?yàn)樵跇?gòu)造函數(shù)中View 的子View還沒(méi)有初始化驱入,findViewByID 為空。需要強(qiáng)調(diào)的是菜單View 在init 的時(shí)候設(shè)置為不可以見(jiàn),這樣在開(kāi)始滑動(dòng)的時(shí)候亏较,菜單View不會(huì)干擾SwipeMenuLayout的滑動(dòng)事件

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        initView();
        menuViewHide();
    }
  
     public void initView() {

        if(mLeftMenuView == null && mLeftMenuViewId != View.NO_ID) {
            mLeftMenuView = this.findViewById(mLeftMenuViewId);
        }

        if(mRightMenuView == null && mRightMenuViewId != View.NO_ID) {
            mRightMenuView = this.findViewById(mRightMenuViewId);
        }

        if(mContextView == null && mContextViewId != View.NO_ID)
            mContextView = this.findViewById(mContextViewId);

        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mOnMenuClickListener != null) {
                    mOnMenuClickListener.onItemClick(v, mPosition);
                }
            }
        });

    }            

2 處理滑動(dòng)事件

重寫(xiě)onTouchEvent 在down 事件中return true莺褒,攔截事件分發(fā),在move 事件中判斷滑動(dòng)距離是否大于閥值雪情,大于閥值遵岩,設(shè)置菜單View 可見(jiàn),移動(dòng)context View巡通,在UP 事件中處理動(dòng)畫(huà)尘执,根據(jù)滑動(dòng)的距離和方向,開(kāi)始對(duì)應(yīng)的動(dòng)畫(huà)扁达。

1. cancle 事件的處理正卧, ListView 的 OnInteruptTouchEvent 會(huì)判斷View 的Y軸滑動(dòng)距離,如果大于一定的距離跪解,攔截事件炉旷,響應(yīng)ListView 的上下滑動(dòng)。按照android 的標(biāo)準(zhǔn)處理叉讥,cancle 按照up 事件處理窘行。

2. ListView 和 SwipeMenuLayout 的事件沖突,如果SwipeMenuLayout 進(jìn)入側(cè)滑后图仓,上下滑動(dòng)距離過(guò)大罐盔,ListView 會(huì)攔截事件,所以一旦進(jìn)入側(cè)滑模式救崔,要禁止ListView 攔截事件.調(diào)用getParent().requestDisallowInterceptTouchEvent(true);
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();

                if(mLeftMenuView != null)
                    mLeftMargin = mLeftMenuView.getWidth();

                if(mRightMenuView != null)
                    mRightMargin = mRightMenuView.getWidth();

                if(mSlideView != null && this != mSlideView && mSlideView.isMenuOpen()) {
                    mSlideView.closeMenu();
                    event.setAction(MotionEvent.ACTION_CANCEL);
                }
                
                Log.d(TAG, "Event ACTION_DOWN mMenuShow:" + mMenuShow);
                super.onTouchEvent(event);
                return true;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) (event.getX() - mDownX);
                if(Math.abs(dx) < mTouchSlop)
                    break;

                if(!mMenuShow ) {
                    menuViewShow(dx);
                    getParent().requestDisallowInterceptTouchEvent(true);
                    mSlideView = this;
                }

                if(dx > 0 && mLeftMenuView == null) break;
                if(dx < 0 && mRightMenuView == null) break;


                if(dx > 0 && dx > mLeftMargin) {
                    dx = mLeftMargin;
                } else if (dx < 0 && dx < -mRightMargin) {
                    dx = -mRightMargin;
                }

                layoutContextView(dx);

                if(mMenuShow)
                    event.setAction(MotionEvent.ACTION_CANCEL);

                return super.onTouchEvent(event);
            case MotionEvent.ACTION_CANCEL:
                Log.d(TAG, "Event ACTION_CANCEL mMenuShow:" + mMenuShow);
            case MotionEvent.ACTION_UP:
                int dis = mContextView.getLeft();
                /**
                 *  dis > 0, move to right, dis < mLeftMargin/2, close menu, dis < mLeftMargin open menu.
                 *  dis < 0, move to left, dis > -mRightMargin/2, close menu, dis > -mRightMargin, open menu
                 */
                if(dis > 0 && mLeftMenuView != null) {
                    if(dis < mLeftMargin/2) {
                        mScroller.startScroll(dis, 0, -dis, 0, mScrollTime);
                    } else if(dis < mLeftMargin) {
                        mScroller.startScroll(dis, 0, mLeftMargin-dis, 0, mScrollTime);
                    }

                    postInvalidate();
                } else if(dis < 0 && mRightMenuView != null) {
                    if(dis > -mRightMargin/2) {
                        mScroller.startScroll(dis, 0, -dis, 0, mScrollTime);  //close
                    } else if(dis > -mRightMargin) {
                        mScroller.startScroll(dis, 0, -mRightMargin-dis, 0, mScrollTime);
                    }

                    postInvalidate();
                }

                mDownX = 0;
                Log.d(TAG, "Event ACTION_UP mMenuShow:" + mMenuShow);
                if(mMenuShow)
                    event.setAction(MotionEvent.ACTION_CANCEL);
                return super.onTouchEvent(event);
            default:
                return super.onTouchEvent(event);
        }

        return super.onTouchEvent(event);
    }

3 contextView 的移動(dòng)和動(dòng)畫(huà)

contextView 的移動(dòng)方式采用layout 方法移動(dòng):

    private void layoutContextView(int dx) {
        if(mContextView != null)
            mContextView.layout(dx, 0, mContextView.getMeasuredWidth()+dx,     mContextView.getMeasuredHeight());

        if(!mResterListener && (dx == mLeftMargin || dx == -mRightMargin)) {
            Log.d(TAG, "registerListener dx:" + dx);
            registerListener(dx);
        } else if (mResterListener && (dx ==0 || (dx > 0 && dx != mLeftMargin ) || (dx < 0 && dx != -mRightMargin))) {
            Log.d(TAG, "unregisterListener dx:" + dx);
            unregisterListener(dx);
        }
    }

當(dāng)滑動(dòng)到一半的時(shí)候手里離開(kāi)惶看,這是菜單要關(guān)閉或者打開(kāi),動(dòng)畫(huà)采用Scroller 的方式六孵,在UP 事件中調(diào)用mScroller.startScroll纬黎, 重寫(xiě)computerScroll

    public void computeScroll() {
        super.computeScroll();

        if(mScroller.computeScrollOffset()) {
            layoutContextView(mScroller.getCurrX());
            postInvalidate();
        }
    }

4 監(jiān)聽(tīng)事件的處理

  1. 定義interface 和 事件注冊(cè),
  2. menu 菜單在init 的時(shí)候已經(jīng)設(shè)置不可見(jiàn)劫窒,在 layoutContextView 的時(shí)候本今,判斷滑動(dòng)距離,如果大于0 設(shè)置菜單menu 可見(jiàn)主巍,如果滑動(dòng)到了菜單寬度的位置冠息,菜單完全打開(kāi),注冊(cè)監(jiān)聽(tīng)事件孕索。避免menu View 和SwipeMenuLayout 滑動(dòng)事件沖突逛艰。
  3. 由于我們攔截了事件,ListView 的 OnItemClick 無(wú)法響應(yīng)搞旭,因此需要我們單獨(dú)寫(xiě)一個(gè)Listener散怖,處理整個(gè)SwipeMenuLayout 的點(diǎn)擊事件響應(yīng)唐断。把這個(gè)事件響應(yīng)放到整個(gè)SwipeMenuLayout,在initView() 函數(shù)中設(shè)置了OnClickListener杭抠,所以在 OnTouchEvent 中每個(gè)事件都調(diào)用了 super.OnTouchEvent()。但是這樣處理的話響應(yīng)滑動(dòng)的同時(shí)也響應(yīng)了OnClickListener恳啥, 需要處理滑動(dòng)和OnClickListener 的事件沖突偏灿,CANCAL 事件來(lái)了,一旦我們進(jìn)入了滑動(dòng)模式钝的,在MOVE 事件中發(fā)送CANCAL事件給super翁垂,OnClickListener就無(wú)法響應(yīng)。
    public interface OnMenuClickListener {

        void onMenuClick(View v, int position);

        void onItemClick(View v, int position);
    }
    
    private void  registerListener(int dis) {
        if(mLeftMenuView != null && dis == mLeftMargin) {
            mLeftMenuView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnMenuClickListener != null)
                        mOnMenuClickListener.onMenuClick(v, mPosition);
                }
            });
        }

        if(mRightMenuView != null && dis == -mRightMargin) {
            mRightMenuView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnMenuClickListener != null)
                        mOnMenuClickListener.onMenuClick(v, mPosition);
                }
            });
        }

        mResterListener = true;
    }

    private void unregisterListener(int dis) {
        if(mLeftMenuView != null && dis > 0) {
            mLeftMenuView.setOnClickListener(null);
        }

        if(mRightMenuView != null && dis < 0) {
            mRightMenuView.setOnClickListener(null);
        }

        mResterListener = false;
    }

5 使用

  1. 定義ListView 的Item XML 注意左右菜單 和context View的ID硝桩;左右菜單根據(jù)需要定義沿猜,不需要不定義即可
  2. 在重寫(xiě)Adapter 的時(shí)候 getView 中設(shè)置position, 重寫(xiě)OnMenuClickListener碗脊,設(shè)置監(jiān)聽(tīng)事件:
    ((SwipeMenuLayout)convertView).setPosition(position);
    ((SwipeMenuLayout)convertView).setOnMenuClickListener(ListViewActivity.this);

三 總結(jié)

不需要重寫(xiě)ListView 和 Adapter啼肩,菜單可以用XML定義好即可,是不是很簡(jiǎn)單衙伶。
傳送門(mén):github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祈坠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子矢劲,更是在濱河造成了極大的恐慌赦拘,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芬沉,死亡現(xiàn)場(chǎng)離奇詭異躺同,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)丸逸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)蹋艺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人椭员,你說(shuō)我怎么就攤上這事车海。” “怎么了隘击?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵侍芝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我埋同,道長(zhǎng)州叠,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任凶赁,我火速辦了婚禮咧栗,結(jié)果婚禮上逆甜,老公的妹妹穿的比我還像新娘。我一直安慰自己致板,他們只是感情好交煞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著斟或,像睡著了一般素征。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萝挤,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天御毅,我揣著相機(jī)與錄音,去河邊找鬼怜珍。 笑死端蛆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的酥泛。 我是一名探鬼主播今豆,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柔袁!你這毒婦竟也來(lái)了晚凿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瘦馍,失蹤者是張志新(化名)和其女友劉穎歼秽,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體情组,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡燥筷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了院崇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肆氓。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖底瓣,靈堂內(nèi)的尸體忽然破棺而出谢揪,到底是詐尸還是另有隱情,我是刑警寧澤捐凭,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布拨扶,位于F島的核電站,受9級(jí)特大地震影響茁肠,放射性物質(zhì)發(fā)生泄漏患民。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一垦梆、第九天 我趴在偏房一處隱蔽的房頂上張望匹颤。 院中可真熱鬧仅孩,春花似錦、人聲如沸印蓖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赦肃。三九已至鼻百,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摆尝,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工因悲, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留堕汞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓晃琳,卻偏偏與公主長(zhǎng)得像讯检,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子卫旱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,183評(píng)論 25 707
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程人灼,因...
    小菜c閱讀 6,426評(píng)論 0 17
  • 讀的書(shū)一直不多,因?yàn)橐恢北桓嬲]:書(shū)是那么尊貴的物品啊顾翼,蘊(yùn)含著知識(shí)的瑰寶投放,從來(lái)舍不得在上面涂一筆一劃,包括身份標(biāo)記的...
    涼夏的海閱讀 288評(píng)論 0 0
  • 廣州适贸,一個(gè)美麗的一線城市灸芳。 我來(lái)到廣州的第一天,去看了有名的廣州塔拜姿。因?yàn)槠渫庑嗡婆素S滿的身材烙样,所以人們又稱...
    MYTERY閱讀 346評(píng)論 0 0
  • 倉(cāng)促的種子無(wú)法在錯(cuò)誤的季節(jié)開(kāi)出花 于是 殘酷的寒冬辜負(fù)了最后的希望 長(zhǎng)河打翻了落日 映出來(lái)的一些余暉 粉飾太平 我...
    卻悔閱讀 272評(píng)論 5 9