Android 自定义View实现SegmentControlView

转载请注明出处:http://blog.csdn.net/a512337862/article/details/72846048

其实,SegmentControlView的效果可以利用RadioGroup+RadioButton实现,也可以利用Layout+Button等布局代码来实现,但这种方法多少有点冗杂。所以最近闲来无事,就通过自定义View实现了这种效果,效果图如下:
这里写图片描述
这里简单的介绍一下思路以及代码实现:

思路

1.首先根据数据源的长度以及布局的宽度获取item的数量以及在canvas上显示的长度
2.遍历数据将item依次在画布的指定位置绘制背景以及数据文本,其中第一个和最后一个一般都有圆角
3.绘制每个item之间分割线以及最外层的边框
4.设置OnTouchListener,通过按下的坐标判断点击了哪个item,重绘布局,并将结果回调

代码实现

自定义属性

attr文件如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SegmentControlView">
        <attr name="textSize" format="dimension"/>
        <attr name="textSelectedColor" format="color" />
        <attr name="textUnSelectedColor" format="color" />
        <attr name="viewSelectedColor" format="color" />
        <attr name="viewUnSelectedColor" format="color" />
        <attr name="roundRadius" format="integer" />
        <attr name="strokeWidth" format="dimension"/>
    </declare-styleable>
</resources>

简单介绍一下各个自定义属性的含义:
1.textSize 文本字体大小
2.textSelectedColor 选中时字体的颜色
3.textUnSelectedColor 未选中时字体的颜色
4.viewSelectedColor 选中时背景色
5.viewUnSelectedColor 未选中时背景色
6.roundRadius 圆角,这里最大值不会超过view宽度和高度的一半
7.strokeWidth 外层边框线条的宽度
这里偷懒没有定义边框的颜色,边框的颜色用了viewSelectedColor,如果需要的话,可以自己添加这个属性,然后在绘制分割线条以及最外层边框代码之前修改一下paint.setColor的值就可以了

自定义View

自定义view代码如下:

/**
 * Created by ZhangHao on 2017/5/24.
 * 自定义仿iOS SegmentControlView
 */

public class SegmentControlView extends View implements View.OnTouchListener {

    //字体大小
    private int textSize;
    //选中时的字体颜色
    private int textSelectedColor;
    //未选中时的字体颜色
    private int textUnSelectedColor;
    //选中时的背景色
    private int viewSelectedColor;
    //未选中时的背景色
    private int viewUnSelectedColor;
    //数据源
    private List<String> values;
    //选中的位置
    private int selectedItem = 0;
    //画笔
    private Paint paint;
    //文本画笔
    private Paint textPaint;
    //用于drawText
    private Rect rect;
    //用于绘制最外层圆角边框
    private RectF rectF;
    //用来绘制圆角
    private RectF roundRectF;
    //path用来绘制第一个和最后一个的半圆角矩形以及最外层圆角边框
    private Path roundPath;
    //边框圆角角度
    private int roundRadius;
    //边框宽度
    private int strokeWidth;
    //点击之后的回调
    private SegmentViewItemClickCallBack callBack;


    public SegmentControlView(Context context) {
        this(context, null, 0);
    }

    public SegmentControlView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SegmentControlView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initDefault();
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SegmentControlView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.SegmentControlView_textSelectedColor:
                    //选中时的字体颜色
                    textSelectedColor = a.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.SegmentControlView_textUnSelectedColor:
                    //未选中时的字体颜色
                    textUnSelectedColor = a.getColor(attr, Color.GREEN);
                    break;
                case R.styleable.SegmentControlView_viewSelectedColor:
                    //选中时的背景色
                    viewSelectedColor = a.getColor(attr, Color.GREEN);
                    break;
                case R.styleable.SegmentControlView_viewUnSelectedColor:
                    //选中时的背景色
                    viewUnSelectedColor = a.getColor(attr, Color.GREEN);
                    break;
                case R.styleable.SegmentControlView_textSize:
                    //字体大小
                    // 默认设置为16sp,TypeValue也可以把sp转化为px
                    textSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.SegmentControlView_roundRadius:
                    // 变宽圆角角度
                    roundRadius = a.getInt(attr, 0);
                    break;
                case R.styleable.SegmentControlView_strokeWidth:
                    // 边框宽度,TypeValue也可以把dp转化为px
                    strokeWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()));
                    break;
            }
        }
        //Recycles the TypedArray
        a.recycle();
        //初始化list
        values = new ArrayList<>();
        //初始化画笔
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(strokeWidth);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);

        //初始化Rect
        rect = new Rect();
        rectF = new RectF();
        roundRectF = new RectF();
        //初始化roundPath
        roundPath = new Path();
        //设置onTouch
        setOnTouchListener(this);
    }

    /**
     * 设置默认值
     */
    private void initDefault() {
        //选中时的字体颜色
        textSelectedColor = Color.WHITE;
        //未选中时的字体颜色
        textUnSelectedColor = Color.GRAY;
        //选中时的背景色
        viewSelectedColor = Color.GREEN;
        //未选中时的背景色
        viewUnSelectedColor = Color.WHITE;
        //字体大小16
        textSize = 10;
        //边框圆角角度
        roundRadius = 20;
        //边框宽度
        strokeWidth = 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取View的宽高
        int width = getWidth();
        int height = getHeight();
        //获取数据长度
        int dataLen = values.size();
        //判断roundRadius,必须小于width/2和height/2
        if (roundRadius > width / 2 || roundRadius > height / 2) {
            roundRadius = width > height ? height / 2 : width / 2;
        }
        //根据数据长度画背景、文本以及分割线(这里数据长度最少为2)
        if (dataLen > 1) {
            //获取每个item的长度
            int itemLen = width / dataLen;
            //设置画笔为填满
            paint.setStyle(Paint.Style.FILL);
            //绘制背景色以及绘制对应的文本
            for (int i = 0; i < dataLen; i++) {
                if (i == selectedItem) {
                    //如果是选中的item
                    paint.setColor(viewSelectedColor);
                    textPaint.setColor(textSelectedColor);
                } else {
                    paint.setColor(viewUnSelectedColor);
                    textPaint.setColor(textUnSelectedColor);
                }
                //绘制背景
                rectF.set(i * itemLen, 0, (i + 1) * itemLen, height);
                if (i == 0) {
                    //第一个
                    roundPath.reset();
                    roundPath.moveTo(rectF.right, rectF.top);
                    roundPath.lineTo(rectF.left + roundRadius, rectF.top);
                    roundRectF.set(rectF.left, rectF.top,
                            rectF.left + roundRadius * 2, rectF.top + roundRadius * 2);
                    roundPath.arcTo(roundRectF, 270, -90);
                    roundPath.lineTo(rectF.left, rectF.bottom - roundRadius);
                    roundRectF.set(rectF.left, rectF.bottom - roundRadius * 2,
                            rectF.left + roundRadius * 2, rectF.bottom);
                    roundPath.arcTo(roundRectF, 180, -90);
                    roundPath.lineTo(rectF.right, rectF.bottom);
                    roundPath.close();
                    canvas.drawPath(roundPath, paint);
                } else if (i == dataLen - 1) {
                    //最后一个
                    roundPath.reset();
                    roundPath.moveTo(rectF.left, rectF.top);
                    roundPath.lineTo(rectF.right - roundRadius, rectF.top);
                    roundRectF.set(rectF.right - roundRadius * 2, rectF.top,
                            rectF.right, rectF.top + roundRadius * 2);
                    roundPath.arcTo(roundRectF, 270, 90);
                    roundPath.lineTo(rectF.right, rectF.bottom - roundRadius);
                    roundRectF.set(rectF.right - roundRadius * 2, rectF.bottom - roundRadius * 2,
                            rectF.right, rectF.bottom);
                    roundPath.arcTo(roundRectF, 0, 90);
                    roundPath.lineTo(rectF.left, rectF.bottom);
                    roundPath.close();
                    canvas.drawPath(roundPath, paint);
                } else {
                    //除了第一个和最后一个其他的直接绘制矩形即可
                    canvas.drawRect(rectF, paint);
                }
                //绘制文本
                rect.set(i * itemLen, 0, (i + 1) * itemLen, height);
                drawTextOnRect(canvas, rect, values.get(i));
            }
            //分割线
            paint.setColor(viewSelectedColor);
            for (int j = 1; j < dataLen; j++) {
                float x = itemLen * j;
                canvas.drawLine(x, 0, x, height, paint);
            }
        }
        //绘制最外层边框(空心矩形)
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(viewSelectedColor);
        //这里stroke是为了让边框能完全显示,如果rectF.set(0,0,width,height),那么边框会有一半因为超出画布范围而无法显示
        int stroke = strokeWidth / 2;
        rectF.set(stroke, stroke, width - stroke, height - stroke);
        roundPath.reset();
        roundPath.moveTo(rectF.left, rectF.top + roundRadius);
        roundRectF.set(rectF.left, rectF.top,
                rectF.left + roundRadius * 2, rectF.top + roundRadius * 2);
        roundPath.arcTo(roundRectF, 180, 90);
        roundPath.lineTo(rectF.right - roundRadius, rectF.top);
        roundRectF.set(rectF.right - roundRadius * 2, rectF.top,
                rectF.right, rectF.top + roundRadius * 2);
        roundPath.arcTo(roundRectF, 270, 90);
        roundPath.lineTo(rectF.right, rectF.bottom - roundRadius);
        roundRectF.set(rectF.right - roundRadius * 2, rectF.bottom - roundRadius * 2,
                rectF.right, rectF.bottom);
        roundPath.arcTo(roundRectF, 0, 90);
        roundPath.lineTo(rectF.left + roundRadius, rectF.bottom);
        roundRectF.set(rectF.left, rectF.bottom - roundRadius * 2,
                rectF.left + roundRadius * 2, rectF.bottom);
        roundPath.arcTo(roundRectF, 90, 90);
        roundPath.lineTo(rectF.left, rectF.top + roundRadius);
        roundPath.close();
        canvas.drawPath(roundPath, paint);
    }

    /**
     * 在指定矩形中间drawText
     *
     * @param canvas     画布
     * @param targetRect 指定矩形
     * @param text       需要绘制的Text
     */
    private void drawTextOnRect(Canvas canvas, Rect targetRect, String text) {
        Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
        // 获取baseLine
        int baseline = targetRect.top + (targetRect.bottom - targetRect.top - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
        // 下面这行是实现水平居中,drawText对应改为传入targetRect.centerX()
        textPaint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(text, targetRect.centerX(), baseline, textPaint);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //存在数据时才判断点击
                if (values.size() > 0) {
                    //记录点击x坐标
                    int x = (int) event.getX();
                    //获取每个item的长度
                    int itemLen = getWidth() / values.size();
                    //根据x判断点击的区域
                    selectedItem = x / itemLen;
                    if (callBack != null) {
                        callBack.onItemClick(v, selectedItem);
                    }
                }
                //重绘
                postInvalidate();
                break;
        }
        return false;
    }

    public int getTextSize() {
        return textSize;
    }

    public void setTextSize(int textSize) {
        this.textSize = textSize;
        postInvalidate();
    }

    public int getTextSelectedColor() {
        return textSelectedColor;
    }

    public void setTextSelectedColor(int textSelectedColor) {
        this.textSelectedColor = textSelectedColor;
        postInvalidate();
    }

    public int getTextUnSelectedColor() {
        return textUnSelectedColor;
    }

    public void setTextUnSelectedColor(int textUnSelectedColor) {
        this.textUnSelectedColor = textUnSelectedColor;
        postInvalidate();
    }

    public int getViewSelectedColor() {
        return viewSelectedColor;
    }

    public void setViewSelectedColor(int viewSelectedColor) {
        this.viewSelectedColor = viewSelectedColor;
        postInvalidate();
    }

    public int getViewUnSelectedColor() {
        return viewUnSelectedColor;
    }

    public void setViewUnSelectedColor(int viewUnSelectedColor) {
        this.viewUnSelectedColor = viewUnSelectedColor;
        postInvalidate();
    }

    public List<String> getValues() {
        return values;
    }

    public void setValues(List<String> values) {
        this.values.clear();
        this.values.addAll(values);
        postInvalidate();
    }

    public void setValues(String[] values) {
        this.values.clear();
        Collections.addAll(this.values, values);
        postInvalidate();
    }

    public void setOnItemClick(SegmentViewItemClickCallBack callBack) {
        this.callBack = callBack;
    }

    public void setCallBack(SegmentViewItemClickCallBack callBack) {
        this.callBack = callBack;
    }
}

Path类是SegmentControlView的一个关键,所有带圆角的部分都是通过Path类来实现的。这里简单介绍一下用到的一些方法:
1. moveTo(float x, float y) : 将起始点设为(x,y),默认情况为(0,0)
2. lineTo(float x, float y) : 从当前点绘制一条线段到(x,y)
3. addArc(RectF oval, float startAngle, float sweepAngle) : 从指定的矩形中绘制一个圆弧,然后从startAngle开始,截取角度为sweepAngle的部分。
这里简单解释一下,如果以矩形oval的中点为坐标轴(0,0)点,那么x轴大于0的部分角度为0,y轴小于0的部分角度为90.
4. reset() : 重置path的所有属性
另外,在绘制最外层边框时,还是需要注意的一点就是 : 绘制有边框的线条时,线条实际的位置是在边框的中间,所有如果直接以canvas的边缘画边框的话,会有一半因为超出canvas的范围无法显示!具体的可以参考:http://blog.csdn.net/a512337862/article/details/74161988

回调接口

/**
 * Created by ZhangHao on 2017/5/27.
 * 用于SegmentView点击之后的回调
 */

public interface SegmentViewItemClickCallBack {
    void onItemClick(View view, int selected);
}

DEMO

这里主要就是activity代码和布局文件代码:

activity

public class SegmentActivity extends Activity implements SegmentViewItemClickCallBack {

    @BindView(R.id.segment_view_1)
    SegmentControlView segmentView1;
    @BindView(R.id.segment_view_2)
    SegmentControlView segmentView2;
    @BindView(R.id.result_tv)
    TextView resultTv;
    @BindView(R.id.segment_view_3)
    SegmentControlView segmentView3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_segement);
        ButterKnife.bind(this);
        initSegmentViews();
    }

    private void initSegmentViews() {
        segmentView1.setValues(new String[]{"首页", "推荐", "好友", "我的"});
        segmentView1.setOnItemClick(this);
        segmentView2.setValues(new String[]{"GAME", "BALL"});
        segmentView2.setOnItemClick(this);
        segmentView3.setValues(new String[]{"首页", "推荐", "好友", "我的"});
        segmentView3.setOnItemClick(this);
    }

    @Override
    public void onItemClick(View view, int selected) {
        switch (view.getId()) {
            case R.id.segment_view_1:
                resultTv.setText("segmentView1 :" + segmentView1.getValues().get(selected));
                break;
            case R.id.segment_view_2:
                resultTv.setText("segmentView2 :" + segmentView2.getValues().get(selected));
                break;
            case R.id.segment_view_3:
                resultTv.setText("segmentView3 :" + segmentView3.getValues().get(selected));
                break;
        }
    }

}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.sona.udv.segmentViews.SegmentControlView
        android:id="@+id/segment_view_1"
        android:layout_width="180dp"
        android:layout_height="44dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        custom:roundRadius="100"
        custom:strokeWidth="1dp"
        custom:textSelectedColor="@android:color/holo_green_dark"
        custom:textSize="16sp"
        custom:textUnSelectedColor="@android:color/holo_orange_dark"
        custom:viewSelectedColor="@android:color/holo_blue_bright"
        custom:viewUnSelectedColor="@android:color/white" />

    <com.sona.udv.segmentViews.SegmentControlView
        android:id="@+id/segment_view_2"
        android:layout_width="150dp"
        android:layout_height="44dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        custom:roundRadius="10"
        custom:strokeWidth="3dp"
        custom:textSelectedColor="@android:color/holo_red_dark"
        custom:textSize="16sp"
        custom:textUnSelectedColor="@android:color/darker_gray"
        custom:viewSelectedColor="@android:color/holo_green_dark"
        custom:viewUnSelectedColor="@android:color/white" />

    <com.sona.udv.segmentViews.SegmentControlView
        android:id="@+id/segment_view_3"
        android:layout_width="150dp"
        android:layout_height="44dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp" />

    <TextView
        android:id="@+id/result_tv"
        android:layout_width="150dp"
        android:layout_height="44dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp" />
</LinearLayout>

这里activity除了展示效果之外,就是用来将对应的SegmentControlView的点击结果显示在TextView上,并没有什么特别的东西。

结语

1.因为本人文字功底有限,所以介绍性的文字很少,但是基本上每句代码都加了注释,理解起来应该不难,如果有任何问题,可以留言。
2.源码github地址 : https://github.com/LuoChen-Hao/BlackHaoCustomView

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值