RecyclerView中裝飾者模式應用

cryAllen發表於2017-03-30

近段時間一直在加班,在趕一個專案,現在專案接近尾聲,那麼需要對過去一段時間工作內容進行復盤,總結下比較好的解決方案,積累一些經驗,我認為的學習方式,是「理論—實踐—總結—分享」,這一種很好的沉澱方式。

在之前專案中,有個需求是這樣的,要顯示書的閱讀足跡列表,具體要求是顯示最近30天閱讀情況,佈局是用列表項佈局,然後如果有更早的書,則顯示更早的閱讀情況,佈局是用網格佈局,如圖所示:

顯示效果

要是放在之前的做法,一般都是ListView,再對特殊item樣式進行單獨處理,後來Android在5.0的時候出了一個RecyclerView元件,簡單介紹下RecyclerView,一句話:只管回收與複用View,其他的你可以自己去設定,有著高度的解耦,充分的擴充套件性。至於用法,大家可以去官網檢視文件即可,網上也很多文章介紹如何使用,這裡不多說。想講的重點是關於裝飾者模式如何在RecyclerView中應用,如下:

  • 裝飾者模式介紹
  • RecyclerView中應用
  • 小結

裝飾者模式介紹

定義:Decorator模式(別名Wrapper),動態將職責附加到物件上,若要擴充套件功能,裝飾者提供了比繼承更具彈性的代替方案。

也就是說動態地給一個物件新增一些額外的職責,比如你可以增加功能,相比繼承來說,有些父類的功能我是不需要的,我可能只用到某部分功能,那麼我就可以自由組合,這樣就顯得靈活點,而不是那麼冗餘。

有幾個要點:

  • 多用組合,少用繼承。利用繼承在設計子類的行為,在編譯時靜態決定的,而且所有子類都會繼承相同的行為,然而,如果用到組合,則可以在執行時動態地進行擴充套件,對一些物件做一些改變。
  • 類應該對擴充套件開發,對修改關閉。
  • 裝飾者和被裝飾物件有相同的超型別。
  • 可以用一個或多個裝飾者包裝一個物件
  • 裝飾者可以在所委託被裝飾者的行為之前或之後,加上自己的行為,以達到特定的目的。
  • 物件可以在任何時候被裝飾,所以可以在執行時動態的,不限量的用你喜歡的裝飾者來裝飾物件。
  • 裝飾模式中使用繼承的關鍵是想達到裝飾者和被裝飾物件的型別匹配,而不是獲得其行為。
  • 裝飾者一般對元件的客戶是透明的,除非客戶程式依賴於元件的具體型別。在實際專案中可以根據需要為裝飾者新增新的行為,做到“半透明”裝飾者。
  • 介面卡模式的用意是改變物件的介面而不一定改變物件的效能,而裝飾模式的用意是保持介面並增加物件的職責。

UML圖:

裝飾者模式UML

RecyclerView中應用

我們既然知道了裝飾者和被裝飾物件有相同的超型別,在做書的閱讀足跡這個頁面的時候,整個頁面外部是一個RecyclerView,比如這樣:

 <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <com.dracom.android.sfreader.widget.recyclerview.FeedRootRecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:drawSelectorOnTop="true"/>

    </android.support.v4.widget.SwipeRefreshLayout>

同時每個Item項裡面又巢狀一個RecyclerView,但外部只有2個Item項,一個Item項代表最近30天要顯示的書的內容,一個Item項是顯示更早書的內容。其中因為涉及到RecyclerView巢狀的問題,所以需要做滑動衝突的相關處理。所以這裡用到自定義擴充套件的RecyclerView,具體解決滑動衝突程式碼如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        final int action = MotionEventCompat.getActionMasked(e);
        final int actionIndex = MotionEventCompat.getActionIndex(e);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
                mInitialTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = (int) (e.getY() + 0.5f);
                return super.onInterceptTouchEvent(e);

            case MotionEventCompat.ACTION_POINTER_DOWN:
                mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
                mInitialTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
                mInitialTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
                return super.onInterceptTouchEvent(e);

            case MotionEvent.ACTION_MOVE: {
                final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
                if (index < 0) {
                    return false;
                }

                final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
                final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
                if (getScrollState() != SCROLL_STATE_DRAGGING) {
                    final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;
                    final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
                    final boolean canScrollVertically = getLayoutManager().canScrollVertically();
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy) || canScrollVertically)) {
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop && (Math.abs(dy) >= Math.abs(dx) || canScrollHorizontally)) {
                        startScroll = true;
                    }
                    return startScroll && super.onInterceptTouchEvent(e);
                }
                return super.onInterceptTouchEvent(e);
            }

            default:
                return super.onInterceptTouchEvent(e);
        }
    }

按照思路就是內部攔截法,也就是RecyclerView自己處理,預設是不攔截,如果滑動距離超過所規定距離,我們就攔截自己處理,設定是可滾動的狀態。

解決完滑動衝突之後,具體看看item項中的佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="vertical"
              android:paddingBottom="4dp"
              android:paddingLeft="10dp"
              android:paddingRight="10dp"
              android:paddingTop="4dp">

    <LinearLayout
        android:id="@+id/head_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="24dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:textColor="@color/black_alpha"
                android:textSize="16sp"
                android:text="@string/zq_account_read_footprint_recent_month"/>

        </RelativeLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_marginTop="8dp"
            android:background="@android:drawable/divider_horizontal_bright"/>

    </LinearLayout>

    <com.dracom.android.sfreader.widget.recyclerview.BetterRecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="12dp"
        android:layout_marginTop="8dp"
        android:orientation="vertical"
        android:scrollbars="none"/>

</LinearLayout>

可以看到,每個Item專案一個頭部,一個RecyclerView。然後是Adapter的適配,這裡就是常用的RecyclerView Adapter的方式,要繼承RecyclerView.Adapter<RecyclerView.ViewHolder>方法,同時要實現onCreateViewHolder、onBindViewHolder、getItemCount、getItemViewType方法,具體程式碼如下:

public class ReadFootPrintAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
    private final static int RECENT_MONTH = 1002;   //最近三十天的Item
    private final static int MORE_EARLY = 1003;     //更早的Item
    
    private Context mContext;
    private List<ReadBookColumnInfo> mColumns;
    private RecentReadAdapter mRecentReadAdapter;
    private MoreEarlyAdapter mMoreEarlyAdapter;
    
    public ReadFootPrintAdapter(Context context){
        this.mContext = context;
        mColumns = new ArrayList<ReadBookColumnInfo>();
        mRecentReadAdapter = new RecentReadAdapter(context);
        mMoreEarlyAdapter = new MoreEarlyAdapter(context);
    }
    
    public void setLoadEnable(boolean loadEnable) {
        mIsLoadEnable = loadEnable;
    }
    
    public void setColumns(List<ReadBookColumnInfo> columns) {
        this.mColumns = columns;
        notifyDataSetChanged();
    }
    
    public void setRecentReadListener(OnOpenBookListener onOpenBookListener){
        mRecentReadAdapter.setOnListener(onOpenBookListener);
    }
    
    public void setMoreEarlyListener(OnOpenBookListener onOpenBookListener){
        mMoreEarlyAdapter.setOnListener(onOpenBookListener);
    }
    
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == RECENT_MONTH){
            View view = LayoutInflater.from(mContext).inflate(R.layout.recycler_read_footprint_recent_month,parent,false);
            return new ColumnViewHolder1(view);
        } if(viewType == MORE_EARLY){
            View view = LayoutInflater.from(mContext).inflate(R.layout.recycler_read_footprint_more_early,parent,false);
            return new ColumnViewHolder2(view);
        }
        else{
            return null;
        }
    }
    
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if(holder instanceof ColumnViewHolder){
            ColumnViewHolder columnViewHolder = (ColumnViewHolder) holder;
            ReadBookColumnInfo readBookColumnInfo = mColumns.get(position);
            if(readBookColumnInfo.getReadBookInfos().size() > 0){
                columnViewHolder.headLayout.setVisibility(View.VISIBLE);
            }
            else{
                columnViewHolder.headLayout.setVisibility(View.GONE);
            }
            columnViewHolder.loadData(readBookColumnInfo);
        }
    }
    
    @Override
    public int getItemCount() {
        return mColumns.size();
    }
    
    @Override
    public int getItemViewType(int position) {
        if (position == 0 )
            return RECENT_MONTH;
        else
            return MORE_EARLY;
    }
    
    private abstract class ColumnViewHolder extends RecyclerView.ViewHolder {
        View headLayout;
        RecyclerView recyclerView;
        
        public ColumnViewHolder(View itemView) {
            super(itemView);
            headLayout = itemView.findViewById(R.id.head_layout);
            recyclerView = (RecyclerView) itemView.findViewById(R.id.recycler_view);
        }
        
        abstract void loadData(ReadBookColumnInfo readBookColumnInfo);
    }
    
    private class ColumnViewHolder1 extends ColumnViewHolder {
        public ColumnViewHolder1(View itemView) {
            super(itemView);
            LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mContext);
            linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
            recyclerView.setLayoutManager(linearLayoutManager);
            recyclerView.setAdapter(mRecentReadAdapter);
        }
        
        @Override
        void loadData(ReadBookColumnInfo readBookColumnInfo) {
            mRecentReadAdapter.setData(readBookColumnInfo.getReadBookInfos());
        }
    }
    
    private class ColumnViewHolder2 extends ColumnViewHolder {
        public ColumnViewHolder2(View itemView) {
            super(itemView);
            GridLayoutManager gridLayoutManager = new GridLayoutManager(mContext,3);
            gridLayoutManager.setOrientation(GridLayoutManager.VERTICAL);
            recyclerView.setLayoutManager(gridLayoutManager);
            recyclerView.setAdapter(mMoreEarlyAdapter);
        }
        
        @Override
        void loadData(ReadBookColumnInfo readBookColumnInfo) {
            mMoreEarlyAdapter.setData(readBookColumnInfo.getReadBookInfos());
        }
    }
}

本來到這裡,基本功能是完成了,可後來產品說要加個上下重新整理,載入更多的操作。需求是隨時可變的, 我們能不變的就是修改的心,那應該怎麼做合適呢,是再增加itemType型別,加個載入更多的item項,那樣修改的點會更多,此時想到了裝飾者模式,是不是可以有個裝飾類對這個adapter類進行組合呢,這樣不需要修改原來的程式碼,只要擴充套件出去,況且我們知道都需要繼承RecyclerView.Adapter,那麼就可以把ReadFootPrintAdapter當做一個內部成員設定進入。我們來看下裝飾者類:

public class ReadFootPrintAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
    private final static int LOADMORE = 1001;
    private final static int NORMAL = 1002;
    
    private RecyclerView.Adapter internalAdapter;
    private View mFooterView;
    
    public ReadFootPrintAdapterWrapper(RecyclerView.Adapter adapter){
        this.internalAdapter = adapter;
        this.mFooterView = null;
    }
    
    public void addFooterView(View footView) {
        mFooterView = footView;
    }
    
    public void notifyDataChanged() {
        internalAdapter.notifyDataSetChanged();
        notifyDataSetChanged();
    }
    
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == LOADMORE){
            return new LoadMoreViewHolder(mFooterView);
        }
        return internalAdapter.createViewHolder(parent,viewType);
    }
    
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if(holder instanceof LoadMoreViewHolder){
            return;
        }else{
            internalAdapter.onBindViewHolder(holder,position);
        }
    }
    
    @Override
    public int getItemCount() {
        int count = internalAdapter.getItemCount();
        if (mFooterView != null && count != 0) count++;
        return count;
    }
    
    @Override
    public int getItemViewType(int position) {
        if(mFooterView != null && getItemCount() - 1 == position)
            return LOADMORE;
        return NORMAL;
    }
    
    public class LoadMoreViewHolder extends RecyclerView.ViewHolder {
        public LoadMoreViewHolder(View itemView) {
            super(itemView);
        }
    }
}

這個Wrapper類就是裝飾類,裡面包含了一個RecyclerView.Adapter型別的成員,一個底部View,到時候在外部呼叫的時候,只需要傳遞一個RecyclerView.Adapter型別的引數進去即可,這樣就形成了組合的關係。具體使用如下:

        mFooterView = LayoutInflater.from(mContext).inflate(R.layout.refresh_loadmore_layout, mRecyclerView, false);
        mReadFootPrintAdapterWrapper = new ReadFootPrintAdapterWrapper(mReadFootPrintAdapter);
        mReadFootPrintAdapterWrapper.addFooterView(mFooterView);
        mRecyclerView.setAdapter(mReadFootPrintAdapterWrapper);

這樣即達到需求要求,又能對原來已有的程式碼不進行修改,只進行擴充套件,何樂而不為。

小結

這雖然是工作中一個應用點,但我想在開發過程中還有很多應用點,用上設計模式。日常開發中基本都強調設計模式的重要性,或許你對23種設計模式都很熟悉,都瞭解到它們各自的定義,可是等真正應用了,卻發現沒有蹤跡可尋,寫程式碼也是按照以前老的思路去做,那樣就變成了知道是知道,卻不會用的尷尬局面。如何突破呢,我覺得事後覆盤和重構很有必要,就是利用專案尾聲階段,空的時候去review下自己寫過的程式碼,反思是否有更簡潔的寫法,還有可以參考優秀程式碼,它們是怎麼寫,這樣給自己找找靈感,再去結合自己已有的知識儲存,說不定就能走上理論和實踐相結合道路上。

閱讀擴充套件

源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard程式碼混淆
3,講講Handler+Looper+MessageQueue關係
4,Android圖片載入庫理解
5,談談Android執行時許可權理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大元件之 " Activity "
10,Android 四大元件之" Service "
11,Android 四大元件之“ BroadcastReceiver "
12,Android 四大元件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命週期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 效能優化
23,Android 訊息機制
24,Android Bitmap相關
25,Android 執行緒和執行緒池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸控事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 外掛化思考
32,開發人員必備技能——單元測試

相關文章