RecyclerView-Adapter用スライドメニューサポートを追加(4編)

16272 ワード

効果図
スライドメニュー
簡単に述べる
前の記事では、現在のAdapterは機能的に強く、いつでも拡張可能な準備ができていることが明らかになりました.この記事では、既存の機能に基づいてスライドメニューのサポートを追加します.まず、この説明の機能は、ヘッダーとfooter以外の機能について説明します.
これらの前提があると、スライドメニューにはどのような表現が必要なのか、どのような機能が必要なのかを考え始めました.
  • クライアントの使用はできません.私たちはAdapterのみに対して、RecyclerViewに対して、クライアントにカスタムコントロールを使用するように強制しません.いつも良い
  • です.
  • スライドの効果はできるだけIosの効果に頼って、なぜIosと違うのか(誰がAndroidを比較させたのか)
  • をテストしないようにします.
  • itemメニューが開いている間に、他の開いているメニューのitemがメニューを閉じる必要があるかどうかは、スイッチ機能
  • が必要である.
  • 左メニュー、右メニュー(現在の実装では両者が同時に存在することはできない)
  • をサポートする.
  • メニューの個数は柔軟で、数量に制限をしない
  • メニューにはイベントコールバック
  • が必要です.
  • その他のメニュー操作api
  • 上記のニーズがあった後、私たちは開発に着手し始めました.まず、イベント処理が欠かせないと同時に、RecyclerViewをカスタマイズするべきではありません.さて次の操作は、この機能を実現するために欠かせないツールViewDragHelperが登場します.これはAndroidシステムに追加されたイベント処理を簡略化するためのツールクラスですが、ここではあまり紹介しないので、知らない自分でgoogleします.
    1.メニュー関連処理インタフェース宣言
    まず、操作インタフェースを定義します.
    メニューパッケージクラス
    public class MenuItem {
        //    
        private int menuLayoutId;
        //    
        @MenuItem.EdgeTrackWhere
        private int edgeTrack;
        //  id
        private int menuId;
    
        @Retention(RetentionPolicy.SOURCE)
        @IntDef({EdgeTrack.LEFT, EdgeTrack.RIGHT}) 
        public @interface EdgeTrackWhere {}
    
        /**
         *       
         */
        public interface EdgeTrack{
            int LEFT = 0;
            int RIGHT = 1;
        }
    }
    

    メニュー作成インタフェース
    必要に応じて2つの方法を提供しますが、どちらの方法もデータが返される場合は組み合わせられるので、場合によってはどちらかを選ぶことをお勧めします.
    public interface ICreateMenus {
        /**
         *       
         * @param viewType         item         
         * @return
         */
        List onCreateMultiMenuItem(int viewType);
        /**
         *       
         * @param viewType         item         
         * @return
         */
        MenuItem onCreateSingleMenuItem(int viewType);
    }
    

    メニュークローズインタフェースおよび構成インタフェース
    public interface ICloseMenus {
        /**
         *     
         */
        void closeMenuItem();
        /**
         *          item
         */
        void closeOtherMenuItems();
        /**
         *           item (           item)
         * @return
         */
        boolean hasOpendMenuItems();
    }
    public interface IMenuSupport {
        /**
         *           menu items
         * @return
         */
        boolean isCloseOtherItemsWhenThisWillOpen();
    }
    

    メニュークリックイベントコールバックインタフェース
    public interface OnItemMenuClickListener {
        /**
         *       
         * @param swipeItemView
         * @param itemView         itemview
         * @param menuView
         * @param position     item    (    )
         * @param menuId         item    id
         */
        void onMenuClick(SwipeLayout swipeItemView, View itemView, View menuView, int position, int menuId);
    }
    

    2.Adapterの拡張開始
    操作インタフェースが定義され、拡張が開始されます.SwipeAdapterという名前を付けて、BaseAdapterから継承させます.これにより、上記のいくつかの機能がすべて備えられます.このようにまずこのAdapterで私たちが何をする必要があるかを考えて、目測は以下の2つの方面の内容を処理する必要があります.その他は必要ありません.a.viewholderの作成は私たちがしなければなりません.元のitemはメニューを追加する必要があるので、itemはb.クリックイベントが必要です.メニューもクリックイベントが必要です.また、私たちがクリックしたこのitemがメニューが開いている状態であれば、閉じる必要があるので、クリック時間は複写する必要があります.
    これで私たちは、public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType)を複写する方法から始めます(前の文章を見たら、この方法がどのように来たのか分かります).
    viewholderの作成
    各itemはメニューを追加する必要があります.私たちが必要とする効果は、元のitemがスライドしている間にメニューが徐々に現れ、自分のメニューが動いていないことです.このように元のitemがメニューの上に完全に覆われているので、ここではFrameLayoutの容器でメニューと元のitemコントロールを包みます.このFrameLayoutから継承されたコントロールはSwipeLayoutと名付けられています.新しいitemとしてviewholderを作成します.したがって、onCreateHolder()で処理する必要がある内容は、新しいitemコントロールの作成、メニューの作成、メニュークリックイベントの処理です.SwipeLayoutの論理は後で話しましょう.上記の説明に従って、次のonCreateHolder()の論理を参照してください.
    @Override
    public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType) {
        View itemView = inflater.inflate(viewType, parent, false);
        MenuItem mi = this.onCreateSingleMenuItem(viewType);
        List mm = this.onCreateMultiMenuItem(viewType);
        //           
        if (null == mi && (null == mm || mm.isEmpty())) {
            return new BaseViewHolder(itemView);
        }
        List menuItems = new ArrayList<>();
        if (null != mi) {
            menuItems.add(mi);
        }
        if (null != mm && !mm.isEmpty()) {
            menuItems.addAll(mm);
        }
        final SwipeLayout swipeLayout = new SwipeLayout(context);
        swipeLayout.setUpView(parent, itemView, menuItems);
        swipeLayout.setIsCloseOtherItemsWhenThisWillOpen(this.isCloseOtherItemsWhenThisWillOpen());
        itemView.setClickable(true);
        BaseViewHolder holder = new BaseViewHolder(swipeLayout, itemView);
        this.initMenusListener(holder);
        return holder;
    }
    

    上記のコードの処理には、a.itemView.setClickable(true);ここのitemViewはクライアントが作成した最も原始的なviewを指し、クリック可能にするのは、このitemViewがイベントを消費しなければならないためであり、そうしないと、このitemはクリックをキャプチャできないからである.b.メニューのあるviewHolderについてnew BaseViewHolder(swipeLayout, itemView);という構造方法を使いましたが、なぜこのように使うのでしょうか.私たちのクリックイベントは、新しく作成されたSwipeLayout itemではなく、クライアントが作成した最も原始的なitemにロードされているということを最初に述べたので、私たちは別のイベントを処理するviewパラメータが必要です.BaseViewHolderのコンストラクタをこのように改造する必要があります
    //  (         )
    public View eventItemView;
    public BaseViewHolder(View itemView) {
        super(itemView);
        this.eventItemView = itemView;
    }
    public BaseViewHolder(View itemView, View eventItemView) {
        super(itemView);
        this.eventItemView = eventItemView;
    }
    
    HeaderFooterAdapterinitItemListenerのイベント処理でクリックイベントを処理するのはitemViewではなく、eventItemViewであり、ダミーコードは以下の通りである.
    protected void initItemListener(final BaseViewHolder holder/*, final int viewType*/){
           holder.eventItemView.setOnClickListener(xxxx);
           holder.eventItemView.setOnLongClickListener(xxxx);
    }
    

    メニューイベント処理
    以下、メニューのクリックイベントがどのように追加されたかを知るだけでよい.メニューとitemの関連関係List> menus = swipeLayout.getMenus();SwipeLayoutに記載される.
    /**
     *          
     * @param holder
     */
    private void initMenusListener(final BaseViewHolder holder) {
        if (! (holder.itemView instanceof SwipeLayout)) {
            return;
        }
        final SwipeLayout swipeLayout = (SwipeLayout) holder.itemView;
        List> menus = swipeLayout.getMenus();
        if (null == menus || menus.isEmpty()) {
            return;
        }
        if (null == this.onItemMenuClickListener) {
            return;
        }
        for (final Pair pair:menus) {
            pair.first.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int hAll = getHeaderViewCount() + getSysHeaderViewCount();
                    final int position = holder.getAdapterPosition() - hAll;
                    onItemMenuClickListener.onMenuClick(swipeLayout, holder.eventItemView, v, position, pair.second.getMenuId());
                }
            });
        }
    }
    

    同時に親クラスのイベント応答処理を複写する必要があり、現在のitemのメニューが閉じている場合にのみイベントに応答でき、コードは貼られません.
    3. SwipeLayout
    上記の説明では、このSwipeLayoutが新しいitem view(元のitem viewとメニューを載せる)であることがわかり、同時にスライドの操作もその上に作用している(イベントの処理が欠かせない).また、ViewDragHelperで事件を処理すると前にも言いました.では、SwipeLayoutは次の作業を完了する必要があります.
    メニューの追加と元のitem viewの関連付けSwipeAdapter AdapterのonCreateHolderでは、swipeLayout.setUpView(parent, itemView, menuItems);を呼び出してSwipeLayoutの初期化を行いました.ここではメニューの操作を簡単に最適化しました.前にメニューが複数サポートされていると言いましたが、ここではメニューコントロールの追加操作はこのように処理されています.メニューが1つしかない場合は、直接追加します.複数のメニューであれば、では、メニューの外側に線形容器が包装されています.メニュー処理のこの部分のコードは比較的に多くて、あまりにも紙面を占めて、しかも技術の含有量がないので、すべて貼るのではなくて、ただ流れを貼るだけでしょう:
    private List> leftMenus;
    private List> rightMenus;
    public void setUpView(ViewGroup viewGroup, View itemView, List menuItems) {
        this.viewGroup = viewGroup;
        this.itemView = itemView;
        if (null == menuItems || menuItems.isEmpty()) {
            return;
        }
        //        
        //1.       
        //2.     
        //3.   item view  
        ....
    }
    

    初期化ViewDragHelper同様に初期化方法では初期化が行われ、各SwipeLayoutはジェスチャー操作を処理する必要があるため、ViewDragHelperを関連付けながら左右メニューに対してViewDragHelper境界処理を行う必要がある
    public void setUpView(ViewGroup viewGroup, View itemView, List menuItems) {
        //      
        ...
        delegate = new SwipeDragHelperDelegate(this);
        this.helper = ViewDragHelper.create(this, 1.0f, delegate);
        delegate.init(helper);
        if (this.EdgeTracking == MenuItem.EdgeTrack.LEFT) {
            helper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
        }else if (this.EdgeTracking == MenuItem.EdgeTrack.RIGHT) {
            helper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
        }
    }
    

    イベント処理SwipeLayoutのイベントをViewDragHelperに依頼して処理する必要があります.ここでの論理は次のとおりです.
  • (ACTION_DOWN操作)を指で押すと、setIsCloseOtherItemsWhenThisWillOpen()というインタフェースの方法がありました.つまり、この時点で他の開いているメニューのitemを閉じることを望むなら、このイベントでは閉じる操作をする必要があります..一部のコードは次のとおりです:
  • if (isCloseOtherItemsWhenThisWillOpen) {
        if (MotionEvent.ACTION_DOWN == action) {
            if (hasOpendMenuItems()) {
                closeOtherMenuItems();
            }
        }
    }
    
  • メニューが開いている間(ACTION_MOVE操作)イベントをブロックする必要はありません.これらのイベントはViewDragHelper処理
  • に渡す必要があります.
  • 指を持ち上げる(ACTION_UP操作)場合、ここでは比較的複雑であり、
  • という効果が期待される.
    a.         ,       item     (  、   ),   view
    b.         ,        item    view,       ,    item  button                ,                    
    c.           ,         ,           
    d.        ACTION_UP          
    

    ここでイベント処理のコントロールはSwipeLayoutであることを知っているので、サブviewがイベントに応答できるかどうかは、イベントを消費する能力があるかどうかに依存する一方で、親コントロールがコントロールをブロックしているかどうかに依存します.以上の説明により、SwipeLayoutのイベントに対する処理ロジックが明確になった.
    private boolean isCloseOtherItemsWhenThisWillOpen = false;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                helper.cancel();
                RectF f = calcViewScreenLocation(itemView);
                boolean isIn = f.contains(ev.getRawX(), ev.getRawY());
                if (isIn && delegate.getMenuStatus() == SwipeDragHelperDelegate.MenuStatus.OPEN) {
                    delegate.closeMenuItem();
                    return true;
                }
                return false;
        }
        if (isCloseOtherItemsWhenThisWillOpen) {
            if (MotionEvent.ACTION_DOWN == action) {
                if (hasOpendMenuItems()) {
                    closeOtherMenuItems();
                }
            }
        }
        return helper.shouldInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        helper.processTouchEvent(event);
        return true;
    }
    public static RectF calcViewScreenLocation(View view) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        return new RectF(location[0], location[1], location[0] + view.getWidth(), location[1] + view.getHeight());
    }
    

    4. SwipeDragHelperDelegate
    イベントをViewDragHelperに依頼した後、ビューの操作をコールバックで処理します.読んでいるあなたがこのクラスの使い方を知っていると仮定します(まだよく分からないのは自分でgoogleできます).まず、私たちが望んでいる効果について説明します.
    スライドコントロールの設定
    メニューコントロールではなくSwipeLayoutの元のitem viewをスライドさせることを期待しているので、swipeLayout.getItemView();が取得したのはクライアントが最も元のitem viewを作成することである.
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        final View itemView = swipeLayout.getItemView();
        if (null != itemView && itemView == child) {
            return true;
        }
        return false;
    }
    

    スライド境界
    コントロールの実際の動作をスライド操作で処理し、予想される範囲内であることを保証する必要があります.例えば、右メニューの例を挙げると、スライドできる最大距離はメニューの幅で、縦はスライドできません.
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        if (swipeLayout.getEdgeTracking() == MenuItem.EdgeTrack.RIGHT) {
            int menuWidth = swipeLayout.getRightMenuWidth();
            if (left > 0 && dx > 0) {
                return 0;
            }
            if (left < -menuWidth && dx < 0) {
                return -menuWidth;
            }
        }
        return left;
    }
    

    スライド動作
    スライド可能な境界が設定された後、demoを実行すると、スライド可能な境界内でどこまでスライドするかがわかります.これは明らかに私たちが望んでいるものではありません.ここでの効果は次のように定義されています.
  • 指を離すと、スライドする前にメニューが閉じると、スライドの距離がメニュー幅の20%を超えると、そのままメニューが開き、そうでないとユーザがメニューを開きたくないと判断すると、メニュー
  • が閉じる.
  • 指を離すと、スライドする前にメニューが開いている場合は、メニュー
  • を直接閉じる.
    //             ,       ,      
    private float openMenuBoundaryPercent = 0.2f;
    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        final View itemView = swipeLayout.getItemView();
        if (releasedChild != itemView) {
            return;
        }
        final int et = swipeLayout.getEdgeTracking();
        final int l = Math.abs(itemView.getLeft());
        final int menuWidth;
        //      
        if (et == MenuItem.EdgeTrack.LEFT) {
            menuWidth = swipeLayout.getLeftMenuWidth();
        }else if (et == MenuItem.EdgeTrack.RIGHT){
            menuWidth = swipeLayout.getRightMenuWidth();
        }else {
            menuWidth = 0;
        }
        final float min = Math.abs(menuWidth * openMenuBoundaryPercent);
        final int left;
        //     
        if (l < min || (MenuStatus.OPEN == this.menuBoundaryStatusOfBeenTo && l < menuWidth)) {
            left = 0;
        } else {
            if (et == MenuItem.EdgeTrack.LEFT) {
                left = +1 * menuWidth;
            }else if (et == MenuItem.EdgeTrack.RIGHT) {
                left = -1 * menuWidth;
            }else {
                left = 0;
            }
        }
        this.helper.settleCapturedViewAt(left, 0);
        this.swipeLayout.invalidate();
    }
    

    ここで補足すると、上記のコードではthis.helper.settleCapturedViewAt(left, 0); this.swipeLayout.invalidate();を用いて位置の設定を行い、内部ではScrollerが実用的であるため、SwipeLayoutでは以下の方法を複写して使用する必要がある.
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (helper.continueSettling(true)) {
            invalidate();
        }
    }
    

    上記の説明およびコードによれば、スライド時に境界条件(ここでは左右の境界を指す)が制限されていることがわかります.つまり、スライド時に最近どの境界に達したかを知る必要があります.
    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
        this.updateMenuStatus(left);
    }
    private void updateMenuStatus(int left) {
        final int et = swipeLayout.getEdgeTracking();
        int menuWidth = 0;
        if(MenuItem.EdgeTrack.LEFT == et) {
            menuWidth = swipeLayout.getLeftMenuWidth();
        }else if (MenuItem.EdgeTrack.RIGHT == et) {
            menuWidth = swipeLayout.getRightMenuWidth();
        }
        //             
        if (left == 0) {
            this.menuBoundaryStatusOfBeenTo = MenuStatus.CLOSED;    }else if (Math.abs(left) >= menuWidth) {
            this.menuBoundaryStatusOfBeenTo = MenuStatus.OPEN;
        }
        //          item
        if (left == 0) {
            this.openView.remove(this.swipeLayout);
        }else if (0 != menuWidth && left == menuWidth) {
            if (!openView.contains(swipeLayout)) {
                openView.add(swipeLayout);
            }
        }
    }
    //            (           ,            )
    @MenuBoundaryStatusOfBeenToWhereprivate int menuBoundaryStatusOfBeenTo = MenuStatus.CLOSED;
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({MenuStatus.OPEN, MenuStatus.CLOSED})
    private @interface MenuBoundaryStatusOfBeenToWhere {}
    /**
     *     
     */
    public interface MenuStatus{
        int CLOSED = -1;
        int DRAGING = 0;
        int OPEN = 1;
    }
    

    スライド領域
    消費可能なイベントのviewが存在する場合、スライド効果は無効になります.この場合、メニューが横スライドのみを定義しているため、ドラッグ可能な領域を限定するには、次の方法を繰り返します(なぜ自分でgoogleしてください):
    @Override
    public int getViewHorizontalDragRange(View child) {
        return swipeLayout.getItemView() == child ? child.getWidth() : 0;
    }
    

    スライドメニューのサポートはここまでで紹介しましたが、書いてあるのは雲の中の霧の中のもので、一部の場所はあいまいであるべきで、見るだけでは全面的に理解するのは難しいと思います.結局、この機能は処理する細部が多いので、まず実現する効果が何なのかを知ってから読んでください.最後はやはりソースコードに移動して、見ればわかるはずです.