程序菜菜AndroidAndroid 开发技巧

RecyclerView中装饰者模式应用

2017-03-30  本文已影响622人  斜杠时光

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

在之前项目中,有个需求是这样的,要显示书的阅读足迹列表,具体要求是显示最近30天阅读情况,布局是用列表项布局,然后如果有更早的书,则显示更早的阅读情况,布局是用网格布局,如图所示:

显示效果

要是放在之前的做法,一般都是ListView,再对特殊item样式进行单独处理,后来Android在5.0的时候出了一个RecyclerView组件,简单介绍下RecyclerView,一句话:只管回收与复用View,其他的你可以自己去设置,有着高度的解耦,充分的扩展性。至于用法,大家可以去官网查看文档即可,网上也很多文章介绍如何使用,这里不多说。想讲的重点是关于装饰者模式如何在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下自己写过的代码,反思是否有更简洁的写法,还有可以参考优秀代码,它们是怎么写,这样给自己找找灵感,再去结合自己已有的知识存储,说不定就能走上理论和实践相结合道路上。

上一篇 下一篇

猜你喜欢

热点阅读