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自定义滚动选择器(二)将从代码的角度带大家来一步步实现该滚动选择器。

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

推荐阅读更多精彩内容