RecyclerView中装饰者模式应用

近段时间一直在加班,在赶一个项目,现在项目接近尾声,那么需要对过去一段时间工作内容进行复盘,总结下比较好的解决方案,积累一些经验,我认为的学习方式,是「理论—实践—总结—分享」,这一种很好的沉淀方式。

在之前项目中,有个需求是这样的,要显示书的阅读足迹列表,具体要求是显示最近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下自己写过的代码,反思是否有更简洁的写法,还有可以参考优秀代码,它们是怎么写,这样给自己找找灵感,再去结合自己已有的知识存储,说不定就能走上理论和实践相结合道路上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,382评论 25 707
  • 做一个类似微信朋友圈的小任务,信心满满地打算使用ListView控件完成,和小组一讨论发现大家都推荐我用Recyc...
    sunnyaxin阅读 2,441评论 3 23
  • 1 场景问题# 1.1 复杂的奖金计算## 考虑这样一个实际应用:就是如何实现灵活的奖金计算。 奖金计算是相对复杂...
    七寸知架构阅读 3,943评论 4 67
  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,372评论 0 27
  • 我们活在浩瀚的宇宙里,漫天漂浮的宇宙尘埃和星河光尘,我们是比这些还要渺小的存在。你并不知道生活在什么时候突然改变方...
    时代姐妹花阅读 395评论 0 0