自定义view系列(7)--SwitchView

自定义view系列(7)--SwitchView

需求描述

草图如下:

草图

公司产品的新版本中要实现一个效果,需求如下: 
- 定义4种状态:未知-休息-上班-下班 
- 要锁定‘未知’状态,锁定之后,未知状态不可点击 
- 用户可以点击除了锁定状态的之外的任意状态,滑块自动滑动到指定状态并触发回调去请求接口,如果请求失败,则控制滑块再滑动到之前的位置。 
- 用户可以滑动滑块到任意位置,如果滑动到锁定状态,要再次滑动重置到之前的状态。如果滑动到其他位置,则触发回调去请求接口,如果请求失败,则控制滑块再滑动到之前的位置。 
- 如果滑块滑动到两个状态之间,则松开手之后,滑块要自动滑动到距离最近的状态上,然后触发回调去请求接口,如果请求失败,则控制滑块再滑动到之前的位置。

实现思路

  1. 由于既有点击操作又有滑动操作,且要实现滑块与下方文本的解耦,所以选择继承ViewGroup实现,这里选择继承LinearLayout。
  2. 文本内容直接使用canvas绘制
  3. 对于锁定位置直接把事件拦截掉
  4. 使用属性动画进行相关的滑动
  5. 剩下的就是一些具体的逻辑处理

最终实现的效果

默认效果

默认效果

自定义文本及字体颜色、大小

效果1

自定义插值器,锁定位置(锁定位置为2),滑动时不展示文本

效果2

所有可用API及作用

API API调用时机 对应的xml属性 作用
setOnSelectedClickListener(OnItemSelectedListener listener) view初始化后的任意时刻 设置item选中监听
setScrollEnable(boolean scrollEnable) view初始化后的任意时刻 app:scrollEnable=”boolean” 设置是否禁用滑动手势【true:不禁用;false:禁用;默认为true】
smoothScrollTo(int position) 需要动态滑动时调用 平滑移动到指定位置【注意与setDefaultSelectedPosition(int position)方法区分开来。】
setShowTextWhenScrolling(boolean isShowText) view初始化后的任意时刻 app:showTextWhenScrolling=”boolean” 滑块在滑动时是否在滑块上显示文本【true:显示;false:不显示;默认为true】
setInterpolator(TimeInterpolator interpolator) view初始化后的任意时刻 设置滑动插值器
setLockPosition(int position) view初始化后的任意时刻 app:lockedPosition=”interger” 指定锁定位置【位置被锁定后,该位置不可点击,且滑块滑动到该位置松开后,会自动滑动到原位置;只能指定一个位置为锁定位置】
unlockPosition() view初始化后的任意时刻 解锁被锁定的位置【解锁后,所有位置都自由了】
getLockPosition() view初始化后的任意时刻 获取被锁定的位置【未设置锁定位置时返回-1】
getTotalItemCount() view初始化后的任意时刻 获取item总数量
setNormalTextColor(int normalTextColor) view初始化后的任意时刻 app:normalTextColor=”color” 设置默认文本颜色【未设置则使用默认】
setNormalTextSize(float normalTextSize) view初始化后的任意时刻 app:normalTextSize=”dimension” 设置默认文本字体大小【未设置则使用默认】
setSelectedTextColor(int selectedTextColor) view初始化后的任意时刻 app:selectedTextColor=”color” 设置选中文本颜色【未设置则使用默认】
setSelectedTextSize(float selectedTextSize) view初始化后的任意时刻 app:selectedTextSize=”dimension” 设置选中文本字体大小【未设置则使用默认】
setSelectedDrawableResId(@DrawableRes int selectedDrawableResId) view初始化后的任意时刻 app:selectedDrawableResId=”integer” 设置选中的item的背景drawable【未设置则使用默认】
setSelectedBgMarginArray(int[] selectedBgMarginArray) view初始化时调用 设置滑块距离四周的margin值【整型数组,位置对应关系:[left,top,right,bottom],数组长度必须为4】
setDefaultSelectedPosition(int position) view初始化时调用 app:defaultSelectedPosition=”integer” 设置滑块初始位置【未设置则使用默认值,即0位置】
setTextArray(String[] textArray) view初始化时调用 app:textArray=”reference” 设置文本内容【使用xml属性时,按照如下方式使用:app:textArray=”@array/customTextArray”】
getSlideView() view初始化后的任意时刻 获取滑块view【目前是TextView,将来可能会继承TextView自定义一个SlideView】
setEnable(boolean enable) view初始化后的任意时刻 是否禁用所有手势【true:不禁用;false:禁用;禁用之后,不在响应任何手势,包括点击和滑动】
isEnable() view初始化后的任意时刻 是否已经禁用所有手势

源码分析

主要说一下实现过程中一些需要注意的地方

1. 测量

在onMeasure()方法中,主要针对MeasureSpec.AT_MOST这种测量模式做了处理:使用默认的宽高设置,对于其他测量模式,统一使用父view传递过来的尺寸。这里需要注意的是使用默认宽高尺寸时,需要与父view传递过来的尺寸作对比,取最小值作为最终的view宽高大小,不然会造成margin值设置失效的问题。

2.绘制

在绘制时,View与ViewGroup是有区别的:对于View,我们在onDraw(Canvas canvas)中绘制,但对于ViewGroup,我们应该在dispatchDraw(Canvas canvas)方法中进行绘制。因为在ViewGroup中,onDraw(Canvas canvas)方法不一定会执行,只有在为ViewGroup设置了背景等情况时,才会执行onDraw(Canvas canvas)方法。而dispatchDraw(Canvas canvas)方法不管是View还是ViewGroup,都会执行。那么既然dispatchDraw(Canvas canvas)方法必定会执行,为什么在View中不使用dispatchDraw(Canvas canvas)方法而使用onDraw(Canvas canvas)方法方法呢?其实主要是为了节省性能,因为只有符合相关条件才会执行onDraw(Canvas canvas)方法,而什么条件下才会执行绘制这些事情Android已经帮我们做好了。 
从View的源码中我们可以看到onDraw(Canvas canvas)和dispatchDraw(Canvas canvas)执行的条件:

  public void draw(Canvas canvas) {
      //...省略部分源码
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }

     //...省略部分源码

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

     //...省略部分源码

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

dispatchDraw(Canvas canvas)是每个ViewGroup都会重写的方法,用来向子view分发绘制事件,所以我们在继承某个ViewGroup重写dispatchDraw(Canvas canvas)方法时,如果我们先调用super.dispatchDraw(canvas),则会先绘制子view,如果后调用super.dispatchDraw(canvas),则会先绘制我们自己的内容。这里的先后顺序要弄清楚。

3.根据任意点的坐标判断该点是否属于某个位置范围内

这里我使用RectF类存储每个item的边界值,RectF类提供了contains方法用来判断任意点坐标是否在范围内:

 private int findPositionByPoint(float x, float y) {
    for (int i = 0; i < mItemBounds.size(); i++) {
        if (mItemBounds.get(i).contains(x, y)) {
            return i;
        }
    }
    //纠偏,防止数组越界
    if (x < mItemBounds.get(0).left) {
        return 0;
    }
    //纠偏,防止数组越界
    if (x >= mItemBounds.get(mItemBounds.size() - 1).right) {
        return mItemBounds.size() - 1;
    }
    return INVALIDATE_POSITION;
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

最后再说两句

  • 第一句:项目地址:SwitchViewProject
  • 第二句:依赖方式参见github中的readme,如果对你有帮助的话,还请不吝赏赐一个star或fork,万分感谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值