转载请注明出处: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