android自定义滚动选择器(一)

本系列文章会详细介绍如何从零开始实现一个滚动选择器,首先看下其效果图,如下所示:

《android自定义滚动选择器(一)》 效果示意图

上面就是本系列文章要实现的自定义滚动选择器,接下来我会从零开始阐述该控件的实现思想。

如果来不及阅读文章,或者想直接获取源码,见git:android自定义滚动选择器

名词解释

这里先对一些名词进行解释,以方便后面可以很容易的理解文章。
(1)item视图:这个item视图就是指滚动选择器中的每一行的视图,一般就是文字,但是我们这里支持自定义,所以理论上可以是各种视图。
(2)分割线:这个是显而易见的,总共有两条,位于这两条中间的item视图就是被选中的视图。
(3)可见的条目数:是指ScrollPickerView一次展示多少条item视图,比如可见的条目数为3,则滚动选择器一次就展示3条item视图。
(4)选中条目:是指在两条分割线中间的item视图。
(5)选中条目的偏移量:是指选中条目的上方还有多少条item视图,比如偏移量设置为1,则选中的条目是第二条,上方有1条item视图;设置为2,则选中的条目是第三条,上方有2条item视图。这个还可以这么理解,即该偏移量也标志着两条分割线的位置,即两条分割线的上面有多少条item,该偏移量就是多少。

目标

我们自定义滚动选择器首先要满足以下目标:

  1. 尽最大化的支持定制,如分割线的颜色、字体的大小、行距等等。
  2. 支持任意数据类型作为数据源,即不能局限于只能使用字符串作为输入。
  3. 支持自定义item视图,用户自己可以定义符合规范的item视图,比如,如果用户想在文字两侧增加icon点缀等等,都可以通过自定义item视图来实现。
  4. 支持用户自定义同时展示的item数目,以及被选中item的偏移量。
  5. 有较高的滚动性能,大量数据时避免卡顿。
  6. 滚动选择器不应受外部大小的控制,应根据item视图来完成自身的高度和宽度适配。

实现

基于以上目标,我们来一步步探讨该如何实现这样的滚动选择器。

实现滚动

首先,滚动选择器自然是要滚动的,那么如何实现滚动呢?这里有两种方案,一种是从头开始自己实现滚动,另一种则是继承android提供的拥有滚动属性的控件。

对于第一种,我们要自己继承view,并自己完成滚动的绘制过程,你可以这么去做,但是任务量和成本非常高,而且实现的兼容性也可能会出现问题,所以不太适合。

那么就剩下第二种了,第二种就是继承当前android体系中已经实现了滚动功能的控件。我们都知道android中有很多滚动的控件,如scrollview、listview、recyclerview等等,那么我们继承哪一个控件呢?答案是显而易见的,那就是继承recyclerview。因为recyclerview相较于其他滚动控件的性能最好,而且有缓存复用机制。

因此,这里我们就采用继承android已有控件RecyclerView,来完成我们视图定义的工作,这里我们将自定义的视图命名为ScrollPickerView,那么其定义如下所示:

public class ScrollPickerView extends RecyclerView {
}

复写RecyclerView中的哪些方法?

这个问题归根到底是我们要实现什么样的需求,前面已经提到了实现目标,下面我们结合实现目标来确认应该实现RecyclerView的哪些方法。

  1. 构造方法

这个是显而易见的,自定义控件要用于xml中必须实现包含有AttributeSet类型入参的构造方法,如下所示:

    public ScrollPickerView(@NonNull Context context) {
        this(context, null);
    }

    public ScrollPickerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScrollPickerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

这样我们自定义的view就可以用于xml中了。

  1. 测量方法onMeasure

为什么要复写这个方法?这个不是RecyclerView已经实现的方法吗?不实现有没有什么关系?

答案是需要进行实现。原因阐述如下:

首先,我们无法控制外部如何使用ScrollPickerView,比如可能高度填充满父视图,也可能是高度只有1dp,那么这个时候如果不复写onMeasure方法,滚动选择器要么填充屏幕进而错乱,要么变得看不到,此时我们必须要保证ScrollPickerView有个合适可用的可视化视图。

而这个可视化视图就是根据item视图的大小来自适应的,因此,我们需要在onMeasure中完成子视图大小的测量,并以此来设置ScrollPickerView的高度和宽度,这样就达到了高宽度自适应,而不因外部设置的属性影响。

  1. 复写onDraw方法

复写onDraw方法的目的主要是完成两条分割线的绘制。我们在自定义view的时候会很方便的通过onDraw方法绘制各种图形,和此处复写onDraw的道理是一样的。

  1. 复写onScrolled方法

因为我们要监听滚动,所以我们要复写onScrolled方法。

  1. 复写onTouchEvent方法

为什么复写该方法?这是因为RecyclerView的滚动我们是无法控制的,也就是说RecyclerView滚动结束后可以停在任何位置,换句话说,其中的item视图也可能会停在任意位置上。但是当前我们的需求是:当RecyclerView滚动结束的时候,必须要将item滚动到合适的位置,比如RecyclerView滚动结束的时候,可能被选中的item没有恰好位于两条分割线中间,那么这个就需要进行调整,使其滚动到分割线中间。这就可以通过监听onTouchEvent中的action up事件来解决。

数据适配器

大家都清楚,RecyclerView需要有个适配器来填充数据,所以我们的ScrollPickerView也必然要有个adapter,我们这里将其定义为ScrollPickerAdapter,那么ScrollPickerAdapter该怎么来设计呢?

首先,ScrollPickerAdapter要实现RecyclerView.Adapter中必须要实现的方法,比如onCreateViewHolder、onBindViewHolder方法等。

其次,ScrollPickerAdapter还应该提供定制化的设置入口,比如设置分割线的颜色、设置偏移量、自定义item视图等,有朋友可能说这个不是ScrollPickerView应该做的功能吗?确实如此,很多时候我们自定义view的时候,往往会在当前view中定义设置入口,而这里我们之所以放在这里来做,有以下理由:

  1. adapter的存在解耦了数据和ScrollPickerView二者的耦合,从这个方面来看,adapter很适合做定制化数据的设置入口。

  2. ScrollPickerView继承自RecyclerView,因此adapter是不可或缺的,在ScrollPickerView内部可以轻易获取adapter,这样就能够拿到adapter的各种数据。

  3. 这样同时可以保持ScrollPickerView的内部简洁,减少其对外暴露,使得ScrollPickerView更加清晰。

最后,adapter还应该对外暴露item点击的点击响应事件,以方便外界监听。

细节设计

这里的细节主要是指设计优化。主要阐述如下。

  1. 我们要支持任意数据类型,所以adapter就必须要泛型化。这样可以接受任意数据类型,如下所示:
public class ScrollPickerAdapter<T> extends RecyclerView.Adapter<ScrollPickerAdapter.ScrollPickerAdapterHolder>
  1. 我们是通过ScrollPickerAdapter来暴露数据定制入口的,虽然可以在ScrollPickerView中获取到adapter,进而获取到这些外部设置的定制数据,但是很显然,获取这些数据的行为并不属于adapter,而且用户也有可能自定义adapter,因此我们这里需要抽象出一个接口,来解耦adapter,这里我们定义接口为IPickerViewOperation,最终adapter的设计如下:
public class ScrollPickerAdapter<T> extends RecyclerView.Adapter<ScrollPickerAdapter.ScrollPickerAdapterHolder> implements IPickerViewOperation 

这样,用户甚至可以自定义adapter,只要实现IPickerViewOperation接口即可。

那么IPickerViewOperation需要具备哪些行为呢?其实就是上面提到的获取定制数据的一些方法,其定义如下所示:

public interface IPickerViewOperation {
    int getSelectedItemOffset();//获取选中item的偏移量

    int getVisibleItemNumber();//获取可见item的数目

    int getLineColor();//获取分割线的颜色

    void updateView(View itemView, boolean isSelected);//滚动的过程中更新视图
}
  1. 我们要滚动选择器支持自定义item视图,但是用户自定义视图各种各样、五花八门,如何保证自定义的item视图能够适配我们的ScrollPickerView呢?然后又如何保证自定义的item视图逻辑可以在滚动选择器这个框架下工作呢?最后又如何将自定义的视图加载到ScrollPickerView中呢?等等…

为了解决这些问题,我们抽象出了一个视图提供接口,用于提供自定义的item视图,当然这个item视图同样需要支持任意数据类型,因此这个接口也必须是泛型化的。该接口定义如下:

public interface IViewProvider<T> {
    @LayoutRes
    int resLayout();//获取布局文件,类似于R.layout.xxx

//对应于adapter中的onBindView
    void onBindView(@NonNull View view, @Nullable T itemData);
//当ScrollPickerView滚动的时候通知视图进行更新
    void updateView(@NonNull View itemView, boolean isSelected);
}
  1. 定制化的数据实际上会影响到ScrollPickerView视图的渲染的,我们都知道,在我们调用setAdapter的时候就会引起视图的重新绘制,因此,为了避免不必要的麻烦,我们直接将adapter以及定制化数据构造完成后再进行setAdapter。这里显然是build设计模式使用的绝佳场景,所以我们为ScrollPickerAdapter提供一个构造器,与此同时将ScrollPickerAdapter的构造方法标识为私有方法,避免外部再通过ScrollPickerAdapter进行数据设置。

至此,自定义滚动选择器的准备工作已基本完成,下篇文章android自定义滚动选择器(二)将从代码的角度带大家来一步步实现该滚动选择器。

    原文作者:寒潇2018
    原文地址: https://www.jianshu.com/p/0accbfd78a20
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞