Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義SwipeLayout實現側滑菜單

自定義SwipeLayout實現側滑菜單

編輯:關於Android編程

先看 SwipeLayout的效果圖

這裡寫圖片描述

圖太多了,我這只上傳一張,想看 listview和GridView效果的,和想看源碼的 —> GitHub

怎麼實現後面說,先說會廢話。

最近整理以前的項目,那時有一個這樣的需求,要在ExpandableListView的ChildView上實現 編輯和刪除的側滑菜單。我當時並沒有用別人的框架,實現出來大概是這個樣子。

這裡寫圖片描述

滑動事件完好,沒有沖突,子view可以獲得點擊事件。

實現起來非常簡單,大概分為三步:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPqLZoaLV4rj2SXRlbbK8vtayydPDSG9yaXpvbnRhbFNjcm9sbFZpZXfAtNf3zqrX7s3isuOyvL7Wo6i21KOsvs3Kx8v8o6zL/L/J0tS64c/yufa2r6Opo6zXotLiIEhvcml6b250YWxTY3JvbGxWaWV3vMyz0LXEysdGcmFtZUxheW91dKOs0uLOttfF1rvE3NPQ0ru49tfTsry+1qGj1NrV4rj219OyvL7WwO+jrLDat8XBvbj219PX07K8vtajrGxlZnTTw9Paz9TKvsTayN2jrHJpZ2h0z9TKvrLLtaWhozwvcD4NCjxwPqLaoaKz9cq8u6/W0LvxyKHGwcS7v+22yDwvcD4NCjxwcmUgY2xhc3M9"brush:java;"> DisplayMetrics dm = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm); mScreentWidth = dm.widthPixels;

在Adapter的getChildView方法中設置leftView的寬度為屏幕寬度

// 設置leftView的大小為屏幕寬度,這樣右邊的rightView就正好被擠出屏幕外
 holder.leftview= convertView.findViewById(R.id.left);
 LayoutParams lp = holder.leftview.getLayoutParams();
 lp.width = mScreentWidth;

③、getChildView方法中給convertView設置Touch監聽事件

 convertView.setOnTouchListener(new View.OnTouchListener()
             {
                 @Override
                 public boolean onTouch(View v, MotionEvent event)
                 {
                     switch (event.getAction())
                     {
                         case MotionEvent.ACTION_DOWN:
                             if (view != null) {
                             //有view被滑開了,點擊其他childview時,用於還原
                                 ViewHolder viewHolder = (ViewHolder) view.getTag();
                                 viewHolder.hsv.smoothScrollTo(0, 0);
                             }
                              break;
                         case MotionEvent.ACTION_UP:
                             ViewHolder viewHolder = (ViewHolder) v.getTag();
                             view = v;
                             // 獲得HorizontalScrollView滑動的水平方向值.
                             int scrollX = viewHolder.hsv.getScrollX();
                             int rightW = viewHolder.rightView.getWidth();
                             if (scrollX < rightW / 4)
                             {  //滑動距離小於右邊布局的1/4收縮
                                 viewHolder.hsv.smoothScrollTo(0, 0);
                             }else
                             {  //展開
                                 viewHolder.hSView.smoothScrollTo(rightW, 0);
                             }
                             break;
                     }
                     return true;
                 }
             });

             // 刪除一條後更新狀態
             if (holder.hsv.getScrollX() != 0) {
                 holder.hsv.scrollTo(0, 0);
             }

簡單說一下,HorizontalScrollView在dispatchTouchEvent的時候,如果發現時橫向滑動就把事件交給onTouchEvent處理,而這個onTouchEvent方法是來自view的(viewGroup也是調用view的該方法),在view的dispatchTouchEvent中,是順序調用OnTouchListener和onTouchEvent的。我們這setOnTouchListener中自己處理了滑動事件,並且返回true,就消耗掉了事件,不會再調用onTouchEvent。

開始說主題,假設都對 view和viewGroup的事件分發機制 、自定義viewGroup 的流程 和 Scroller 都有了初步的了解。如果沒有,我們就假設有!

這裡寫圖片描述

 

1.自定義SwipeLayout

我是繼承LinearLayout實現的,為什麼不繼承HorizontalScrollView或是ViewGroup?
HorizontalScrollView其實就是一個實現滾動功能的FrameLayout,view的onMeasure和onLayout是層層實現的,我不能在HorizontalScrollView的onLayout方法中對其子view的子view直接設置為屏幕寬度。

繼承ViewGroup就要自己測量和擺放子view,開什麼玩笑,我這麼懶得人。

為了節省大家寶貴的時間,下面只說重點。

1.1 事件沖突解決

為什麼有事件沖突呢? 我們知道listview是上下滑動的,而我們的這個側滑布局要左右滑動。當我們屏幕上橫向滑動時,只要稍微斜了一點,那麼listview就認為要上下滑動,它就把滑動事件攔截了自己交給自己的onTouchEvent處理。根本都分發不到SwipeLayout的局部中。產生效果:側滑出來一點劃不動了,卡住了…

然而google早已看穿了一切,他們給我們提供了這個方法。

requestDisallowInterceptTouchEvent(true);

看過源碼的伙伴肯定知道,這其實就是設置一個標志位。當傳入true時,駁回父view的中斷請求。( 就是父view不能中斷事件必須分發到子view。)

那麼事件就由我們處理了,因為listview需要上下滑動事件,而SwipeLayout需要左右滑動事件,剛好各取所需,復寫dispatchTouchEvent(MotionEvent ev)方法來實現各取所需。

public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                /**
                 * 不允許父view對觸摸事件的攔截
                 */
                disallowParentsInterceptTouchEvent(getParent());
                startX = ev.getX();
                startY = ev.getY();
                isHorizontalMove =false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(!isHorizontalMove){
                curX = ev.getX();
                curY = ev.getY();
                float dx = curX - startX;
                float dy = curY - startY;
                    /**
                     * 認為發生了滑動
                     */
                    if(dx*dx+dy*dy > mTouchSlop*mTouchSlop){
                        /**
                         * 垂直滑動
                         */
                        if (Math.abs(dy) > Math.abs(dx)){
                            /**
                             * 允許父view對觸摸事件攔截,讓其他view去處理事件
                             */
                            allowParentsInterceptTouchEvent(getParent());
                            /**
                             * 垂直滾動復原所有item
                             */
                            shrinkAllView();
                        }else{
                            /**
                             * 水平滑動,攔截來自己處理
                             */
                            isHorizontalMove = true;
                            /**
                             * 為了在onTouchEvent的Move事件中第一次模擬滑動距離不要太大,
                             * 記錄上一次發生move的位置
                             */
                            lastX = curX;
                        }
                    }
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

在ACTION_DOWN中 駁回父view攔截,那麼我們就可以開心的在ACTION_MOVE對事件進行處理(先判斷是否發生了滑動,再判斷是 上下 還是 左右,是上下就取消對父類的駁回,讓listview去處理事件,同時讓所有劃開的SwipeLayout關閉。是左右滑動就 中斷繼續往下層分發,攔截到自己的onTouchEvent做事件處理)。

中斷分發:

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(isHorizontalMove){
            /**
             * 發生水平滑動,把事件中斷到本層onTouchEvent中處理
             */
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

我這側滑菜單裡是兩個imageView默認是不可clickable的,所以就算不攔截最後也會執行我的onTouchEvent處理。但是如果是Button,ImageButton等默認是可點擊的,那麼事件就傳不上來了。

因為我們不知道是哪個父view對事件進行了攔截,所以要循環遞歸設置標志位(為了更好的兼容性,反正我這是listview攔截了)。

  /**
     * 因為不知道是父view那一層會攔截觸摸事件,所以遞歸向上設置標志位
     * 直到頂層view,就直接返回
     */
    private void disallowParentsInterceptTouchEvent(ViewParent parent) {
        if (null == parent) {
            return;
        }
        parent.requestDisallowInterceptTouchEvent(true);
        disallowParentsInterceptTouchEvent(parent.getParent());
    }
    private void allowParentsInterceptTouchEvent(ViewParent parent) {
        if (null == parent) {
            return;
        }
        parent.requestDisallowInterceptTouchEvent(false);
        allowParentsInterceptTouchEvent(parent.getParent());
    }

1.2 onTouchEvent處理

當標志位表明是橫向滑動時,我們需要在ACTION_MOVE裡面模擬滾動。觸發一次ACTION_MOVE就讓內容滾動一點點,實現效果就是內容跟隨手指移動。

 public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if(isHorizontalMove){
                    curX = ev.getX();
                    float dX = curX-lastX;
                    /**
                     * 不斷更新lastX的位置,用於模擬滑動
                     */
                    lastX = curX;
                    /**
                     * 滑動的距離與實際相反,因為滾動的時候移動的是內容,不是view
                     */
                    int disX = getScrollX() + (int)(-dX);
                    /**
                     * 手指向右移動
                     */
                    if(disX<0){
                        /**
                         * 如果菜單收縮,防止越界(越界後ACTION_UP又會滾動回來,但還是不越界的好)
                         * 如果菜單展開,,我們希望迅速關閉菜單,不需要模擬滾動
                         */
                        scrollTo(0, 0);
                    }
                    /**
                     * 手指向左移動,如果累加的移動距離已經大於menu的寬度,就讓menu顯示出來。
                     * 如果移動距離還不到,就模擬滾動
                     */
                    else if(disX>rightViewWidth){
                        scrollTo(rightViewWidth,0);
                    }
                    else{
                        scrollTo(disX, 0);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                float endX = ev.getX();
                float dis =endX -startX;
                /**
                 * 手指向左滑動,模擬展開
                 */
                if(dis<0){
                    SimulateScroll(EXPAND);
                }
                /**
                 * 手指向右滑動,模擬關閉
                 */
                else{
                    SimulateScroll(SHRINK);
                }
            default:
                break;
        }

        return true;
    }

先說一下這個lastX,這個坐標是onInterceptTouchEvent中判斷為橫向移動後的最近坐標。(事件不能被消費掉,但是會隨著時間消失,我們可以在最內層到最外層的所有view的onTouchEvent中對一個事件進行處理,全部返回false。但是這裡是 在onInterceptTouchEvent中已經觸發了11個(大概)ACTION_MOVE事件,然後onTouchEvent才進行處理。簡單的說就是我滑動的前一段距離拿去做判斷了,判斷好了才跟這手指移動。)

那這麼辦呢?要麼

 float dX = curX-lastX;

滑動順暢,不足的距離由最後scroller模擬滾動補回。

 float dX = curX-startX;

剛發生移動那一下移動約10個ACTION_MOVE事件的距離,效果不好。

1.2.1 onTouchEvent的ACTION_MOVE中模擬滾動

給不了解的惡補一下概念:

這裡寫圖片描述

getX() 和getY()是view的內部坐標,大小補回超過長寬。

這裡寫圖片描述

getScrollX()

獲取的是view左上角到內容左上角的距離。至於正負表示方向。
Positive numbers will scroll the content to the left.  

 屏幕剛顯示,未發生移動之前getScrollX()等於0,每一次scrollTo後會刷新getScrollX()的值。

 在一次ACTION_MOVE事件中,手指移動了dx距離,那麼讓內容也一起移動dx,就實現了跟隨手指移動的效果。

相當於 getScrollX()+=dx。

1.3 用scroller模擬繼續滑動

當我們手指拿起來的時候,要判斷SwipeLayout的菜單欄是應該收縮還是應該展開。當確定了狀態後就要模擬手指觸摸滑動到指定位置。

  /**
     * move事件裡模擬滑動完成後,判斷展開狀態
     * 再模擬滾動到目標位置
     */
    public void SimulateScroll(int type){
        int dx =0;
        switch (type){
            case EXPAND:
            //手指向左滑動getScrollX為正
                dx = rightViewWidth-getScrollX();
                break;
            case SHRINK:
             //手指向右滑動getScrollX為負
                dx = 0-getScrollX();
                break;
            default:
                break;
        }
        scroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx)/2);
        invalidate();
    }

    @Override
    public void computeScroll() {
        /**
         * Call this when you want to know the new location.  If it returns true,
         * the animation is not yet finished.
         *
         * 返回true代表正在模擬數據,false 已經停止模擬數據
         */
        if (scroller.computeScrollOffset()) {
            /**
             * 更新X軸的偏移量
             */
            scrollTo(scroller.getCurrX(), 0);
            /**
             * 遞歸調用computeScroll()方法,直到模擬滾動完成
             */
            invalidate();
        }
    }

 這裡調用invalidate()重繪UI,會再次調用computeScroll(),遞歸 直到 模擬滾動完成。

2. 上下滑動和刪除時狀態改變

listview滑動時所有SwipeLayout復原。
我們知道listview刪除了一個item,不一定會回收view,有可能只是重新裝載了數據。那麼顯示的時候要SwipeLayout復原。

  /**
     * 用於上下滑動和刪除item時的,狀態改變
     */
    static List swipelayouts = new ArrayList<>();
    public  static void addSwipeView(SwipeLayout v){
            if(null==v){
                return;
            }
            swipelayouts.add(v);
        }
        public static void removeSwipeView(SwipeLayout v){
            if(null==v){
                return;
            }
            v.SimulateScroll(SwipeLayout.SHRINK);
        }
        private void shrinkAllView(){
            for(SwipeLayout s :swipelayouts){
                if(null==s){
                    swipelayouts.remove(s);
                    continue;
                }else {
                    s.SimulateScroll(SwipeLayout.SHRINK);
                }

            }
        }

內部定義了三個方法,當刪除item時調用removeSwipeView方法使該view復原。在adapte的getview方法添加新item時調用addSwipeView,把當前顯示的所有SwipeLayout都裝在這個list裡面。在上面dispatchTouchEvent中判斷為上下滑動事件的時候調用shrinkAllView復原所有。

基本上就完了,有木有很簡單!哈哈

3. 其他

我們這裡是解決了上下滑動和左右滑動的沖突,那麼listview 中item為scrollview時,兩個都要豎著滑動。
我們為什麼想要在listview 中使用scrollview,因為我們的item中需要顯示的太多。我們知道當scrollview裡的內容高度 小於它父view給他的高度的時候,它是完全展開的,不需要也不能滑動。那麼使listview給item足夠大的高度,讓scrollview不必以滾動的方式來展現,就可以解決這個滑動沖突,反正滾動事件也會被listview 攔截。
新建一個新的NoScrollListView繼承與listview,並復寫onMeasure方法。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       //這好比你有一個炒雞有錢的爹,你想要多少都能滿足你,對! 是滿足你...
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 7, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

那麼我有一個問題,viewpage+fragment+listview +SwipeLayout,我們知道viewpage要左右滑動,當發生滑動事件的時候,我是該移動SwipeLayout呢?還是讓viewpage子去翻頁呢?

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved