美文网首页
由 View.getParent() 的设计想到的一种列表实现

由 View.getParent() 的设计想到的一种列表实现

作者: 你可记得叫安可 | 来源:发表于2020-05-30 14:36 被阅读0次

    View.getParent() 的设计思想

    不知道有没有人注意到,当我们使用 View.getParent() 时,其返回的 ParentView 并不是一个真正的 View,而是一个 ViewParent 接口。
    这样做的设计思想是:一个 View 有非常多的 public 方法,ParentView 只希望暴露方法的有限集合给 ChildView,这样能够避免 ChildViewParentView 的修改影响到其他的兄弟 BrotherView

    项目背景

    现在我们项目中有这样的场景:有一个展示列表,列表中的每一项 ItemView 都是一种固定样式的 xml 布局,只是每个 ItemView 的内容和行为不一样。
    比如下图所示,第一行是一种标题 TitleItemView,它只显示标题文案。第二、三行是一种内容 ContentItemView,左边是描述文案,右边是操作按钮。

    image.png

    项目实践中,这个列表并不是一个动态无限长的列表(通过网络不断加载更多列表项)。它是一个静态有限长的的列表,但是需要满足需求的不断扩展(不断添加新的静态列表项)。这些不同样式的 ItemView 相互交错,很自然地我们想到使用 RecyclerView

    传统的写法

    我们传统的 RecyclerView 写法是在 Adapter.onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) 中,根据 position 找到 Adapter 的数据集中对应的数据,然后将数据设置到 holder#itemView 中进行展示。展示数据的变化则是通过外部的业务类进行更新,然后将变化通知给 Adapter,有它通过 onBindViewHolder() 来更新 itemView 中的展示数据。

    缺点
    1. 约束问题

    官方的实现中 ViewHolder#itemViewpublic 的。因此可能存在这样的情况:列表中的一个 itemA 改变了 TitleItemView 的一个属性,比如 alpha 属性,当另一个 itemB 复用这个 TitleItemView 时,无法知道它的 alpha 属性已经被改过了。因为这两个 item 虽然有一样的布局样式,但是由于业务的不同,导致业务的数据会进行不一样的展现。要约束对标题类型 TitleItemView 的操作就要么使用文档,要么口头相传的形式来告诉开发者:这种类型的 itemView 哪些属性可能会被改变,使用前需要将它们全部重置一遍。

    2. View ViewModel 的绑定问题

    列表中数据的改变需要来自 Adapter 外部,而 ItemView 的实例化却在 Adapter.onCreateViewHolder 中。这样就导致每一个列表项的业务 ViewModel 与它对应的 ItemView 并不是强相关,它们两个之间通过数据位置 position 来联系起来。即 ViewModel 改变数据集中 position 位置的数据,Adapter 收到数据集更新的通知,将 position 位置的数据通过 onBindViewHolder() 方法更新到 UI 上面。

    解决方案

    问题 1 可以通过上面类似 ViewParent 的设计来解决:一种类型的 ItemView 通过实现特定类型的接口,来暴露特定类型的修改 View 的方法。

    • 设计一个基类接口,它包含了所有 ItemView 的通用操作
    public interface SettingItemViewInterface {
        /**
         * RecyclerView viewHolder 解绑的时候会调用这个方法, 子类需要在这个方法内恢复ui到xml设置的状态
         */
        void unBind();
    
        /**
         * 整个View是不是enable态
         * @param enabled
         */
        void setEnabled(boolean enabled);
    
        /**
         * 整个View是不是enable态
         */
        boolean isEnabled();
    }
    

    enabled 容易理解,这里不说了。暴露一个 unBind() 接口,每个类型的 ItemView 实现该方法,根据自己设定的初始状态将 ItemView 的状态重置掉(因为只暴露了特定修改 ItemView 的方法,因此一定能在 unBind() 方法中将 ItemView 的状态重置)
    下面我们看一个具体的类型接口。
    定义标题类型的接口,可以注意到 它只暴露了有限几个方法给开发者进行使用。比如,通过该接口就限制了开发者改变 SettingItemTitleViewalpha 值:

    @Implement(view = SettingItemTitleView.class)
    public interface SettingItemTitleViewInterface extends SettingItemViewInterface {
    
        /**
         * 左侧TextView 文字
         * @param charSequence
         */
        void setLeftText(CharSequence charSequence);
    
        /**
         * 左侧TextView 文字
         * @param stringId
         */
        void setLeftText(@StringRes int stringId);
    
        /**
         * 左侧文字颜色
         * @param color
         */
        void setLeftTextColor(@ColorInt int color);
        ...
    }
    

    代码中的 @Implement 注解可以先不管,它并不会生成中间胶水代码,只是标注一下该接口与其对应的实现类,从而能够通过接口快速找到对应实现类。
    这样设计的原因是:我们的初衷是将 ItemView 的所有操作都变为面向接口的操作,因此 Adapter 中能够看到的应该也只有接口。这个注解就是为了方便 Adapter 通过接口找到对应的 ItemView 类,然后通过反射将 ItemView 实例化。关于 Adapter 中具体如何使用,会在下面的小节中讲到。

    下面我们再看一看 SettingItemTitleViewInterface 所对应的 ItemView

    public class SettingItemTitleView extends RelativeLayout implements SettingItemTitleViewInterface {
    
        private final static float ENABLED_ALPHA = 1.0f;
        private final static float DISABLED_ALPHA = 0.5f;
    
        private TextView mLeftText;
    
        public SettingItemTitleView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
            inflate(context, R.layout.setting_ui_2_item_title_layout, this);
    
            mLeftText = findViewById(R.id.setting_ui_2_item_title_left_tv);
        }
    
        @Override
        public void unBind() {
            mLeftText.setText(R.string.setting_ui_na);
            mLeftText.setTextColor(ContextCompat.getColor(getContext(), R.color.setting_ui_2_item_main_text_color));
        }
    
        @Override
        public void setLeftText(CharSequence charSequence) {
            mLeftText.setText(charSequence);
        }
    
        @Override
        public void setLeftText(int stringId) {
            mLeftText.setText(stringId);
        }
    
        @Override
        public void setLeftTextColor(int color) {
            mLeftText.setTextColor(color);
        }
    
        @Override
        public void setEnabled(boolean enabled) {
            super.setEnabled(enabled);
            mRightText.setEnabled(enabled);
            mRightText.setAlpha(enabled? ENABLED_ALPHA : DISABLED_ALPHA);
        }
        ...
    }
    

    SettingItemTitleView 对外只能通过接口 SettingItemTitleViewInterface 进行操作。Adapter 会在复用这个 SettingItemTitleView 之前调用 unBind() 方法将接口所操作的 View 属性全部重置回了默认值

    问题 2 我们就需要设计一种数据结构 SettingItemStandardViewController 接口,列表中的每一项 Item 都是 SettingItemStandardViewController 的实例。
    从我们的设计思路上,我们要使 SettingItemViewModel 与具体的 ItemView 绑定,但是又只能通过 SettingItemViewInterface 来操作对应的 ItemView,那么就需要有一个中间类通过持有 SettingItemViewModelSettingItemViewInterface 两个引用,使它们两个之间建立绑定关系。SettingItemStandardViewController 就是这样的中间类,它本质上是 SettingItemViewInterface 的代理。
    SettingItemStandardViewController 通过方法 onBindViewHolder()viewInterface 传入进来,这样 SettingItemStandardViewController 就成为了 viewInterface 的代理。然后 SettingItemStandardViewController 生成对应 ItemViewViewModel

    public abstract class SettingItemStandardViewController<VM extends SettingItemViewModel, V extends SettingItemViewInterface>
            extends SettingItemViewController<VM> {
        private Context mContext;
    
        @CallSuper
        public void onBindViewHolder(Context context, V viewInterface, CompositeDisposable compositeDisposable, int position) {
            mContext = context;
            if (mViewModel == null) {
                mViewModel = generateViewModel();
            } else {
                mViewModel.onUnbindView();
            }
            if (mViewModel != null) {
                mViewModel.onBindView();
            }
        }
    
        protected abstract VM generateViewModel();
        ...
    }
    

    上面的 onBindViewHolder 方法会在 Adapter.onBindViewHolder 中被调用。在使用 mViewModel 之前先进行 onUnbindView() 清理原来的订阅关系,然后调用 onBindView 来初始化新的数据。

    我们来看一个具体的设备名称条目的实现:

    public class SettingDeviceNameViewController extends SettingItemStandardViewController<SettingDeviceNameViewModel, SettingItemTitleViewInterface> {
    
        private SettingDeviceNameViewModel mItemViewModel;
    
        // 因为每一个 Controller 都是一个具体业务的 Controller,因此它能够生成改业务对应的 ViewModel
        @NonNull
        @Override
        protected SettingDeviceNameViewModel generateViewModel() {
            mItemViewModel = new SettingDeviceNameViewModel();
            return mItemViewModel;
        }
    
        @Override
        public void onBindViewHolder(Context context, SettingItemTitleViewInterface viewInterface, CompositeDisposable compositeDisposable, int position) {
            super.onBindViewHolder(context, viewInterface, compositeDisposable, position);
    
            viewInterface.setLeftText(R.string.setting_equipment_name_list);
    
            compositeDisposable.add(mItemViewModel.getNameObservable()
                    .subscribe(viewInterface::setLeftText));
        }
    }
    

    使用

    最后我们来看看上面这些类是如何联系起来的。

    class PageAdapter extends RecyclerView.Adapter<SettingItemViewHolder> {
        private Context context;
        private List<SettingItemViewController<?>> controllerList;
        private SparseArrayCompat<Class> customViewTypeMap = new SparseArrayCompat<>();
    
        public PageAdapter(Context context, List<SettingItemViewController<?>> controllerList) {
            this.context = context;
            this.controllerList = new ArrayList<>(controllerList);
            create();
        }
    
        protected void create() {
            for (SettingItemViewController controller : controllerList) {
                controller.onCreate();
            }
        }
    
        protected void destroy() {
            for (SettingItemViewController controller: controllerList) {
                controller.onDestroy();
            }
            controllerList.clear();
        }
    
        @Override
        public void onViewRecycled(SettingItemViewHolder holder) {
            super.onViewRecycled(holder);
            holder.getCompositeDisposable().clear();
        }
    
        @Override
        public int getItemViewType(int position) {
            SettingItemViewController controller = controllerList.get(position);
            ParameterizedType parameterizedType = (ParameterizedType) controller.getClass().getGenericSuperclass();
            if (parameterizedType != null) {
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    Class genericClass = (Class) actualTypeArgument;
                    if (SettingItemViewInterface.class.isAssignableFrom(genericClass)) {
                        Implement annotation = (Implement) genericClass.getAnnotation(Implement.class);
                        if (annotation != null) {
                            int viewType = annotation.view().hashCode();
                            customViewTypeMap.put(viewType, annotation.view());
                            return viewType;
                        }
                    }
                }
            }
            throw new RuntimeException("Undefined ViewType");
        }
    
        @Override
        public SettingItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            Class clazz = customViewTypeMap.get(viewType);
            if (clazz == null) {
                throw new RuntimeException("onCreateViewHolder: undefined viewType");
            }
            try {
                Constructor constructor = clazz.getConstructor(Context.class, AttributeSet.class);
                constructor.setAccessible(true);
                View v = (View) constructor.newInstance(context, null);
                if (v.getLayoutParams() == null) {
                    v.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
                }
                return new SettingItemViewHolder(context, v);
            } catch (IllegalAccessException | java.lang.InstantiationException |
                        NoSuchMethodException | InvocationTargetException e) {
                e.printStackTrace();
                throw new RuntimeException("onCreateViewHolder: view cannot be inflated = "
                            + clazz.getSimpleName() + "\n" + DJILogUtils.exceptionToString(e));
            }
        }
    
        @Override
        public void onBindViewHolder(SettingItemViewHolder holder, int position) {
            // 把重用的viewHolder中的原有订阅关系取消
            holder.getCompositeDisposable().clear();
            SettingItemViewController<?> controller = controllerList.get(position);
            
            if (controller instanceof SettingItemStandardViewController) {
                ((SettingItemViewInterface) holder.itemView).unBind();
                ((SettingItemStandardViewController) controller).onBindViewHolder(
                        holder.itemView.getContext(),
                        (SettingItemViewInterface) holder.itemView,
                        holder.getCompositeDisposable(),
                        position);
            }
        }
    
        @Override
        public int getItemCount() {
            return controllerList.size();
        }
    }
    
    • controllerList:这个就是需要展示的列表 Item List。每一个具体项都是一个具体的 SettingItemViewController,例如上面的 SettingDeviceNameViewController

    • getItemViewType 方法:遍历 Adapter 所持有的 controllerList 中的每一个 controller,通过反射方法找到泛型中所写对应的 viewInterface。然后查找这个 viewInterface@Implement 注解,找到对应的 ItemView.class。将 ItemView.classhashCode 作为一种 ViewTypeId 返回,并且将 [ViewTypeId, ItemView.class] 的映射缓存下来,用于之后在 onCreateViewHolder() 中根据 ViewTypeId方便地找到 ItemView.class。这个方法收集了所有的 ItemViewType

    • onCreateViewHolder 方法:通过方法参数 viewType[ViewTypeId, ItemView.class] 中找到需要创建的 ItemView,然后反射调用它的构造方法创建 ItemView 的实例。最终返回统一的 SettingItemViewHolder

    • onBindViewHolder 方法:这个是 RecyclerView 通过 ViewHolder 复用 ItemView 的关键方法。将 holder.itemView 强制转换为 SettingItemViewInterface,然后调用 unBind() 方法将 holder.itemView 的状态重置为初始状态。接着根据方法中传入的 position 获取到需要被重新绑定的 SettingItemViewController,强转为 SettingItemStandardViewController 后,调用 onBindViewHolder()holder.itemView 以接口的形式注入。之后这条设置项的逻辑就全部都在,比如 SettingDeviceNameViewController 这样的具体 ViewController 中运行了。

    • onViewRecycled 方法:我们在这里将 ViewController 中的所有被 holder.getCompositeDisposable() 管理的订阅关系解除。这样不可见的 ItemView 就不会在后台运行业务逻辑。

    相关文章

      网友评论

          本文标题:由 View.getParent() 的设计想到的一种列表实现

          本文链接:https://www.haomeiwen.com/subject/mgjaahtx.html