1.概述
Android中关于控制开关和页面/状态切换的使用场景还是比较多的。源生做的支持也有比如RadioGroup 和Tabhost等。这里准备通过自定义View来模仿学习下IOS两种常见UI样式: SwitchButton 和 SegmentControl。
首先先通过简易的组装View来实现两种UI的相应效果,其次呢,尝试通过绘制来达到同样的更灵活的样式。代码前后共实现按钮切换和页面切换两个样式,三种实现方案,其中,两种SwitchButton实现,一种SegmentControl实现。实现方案中关于自定义View绘制,本篇只讲述SwitchView,希望大家能举一反三,同样做到SegmentControl的相同效果。个人也更倾向于使用自定义实现,更方便灵活。
先看效果图:
头部即为切换页面的SegmentControl,然后第一行是通过组装view来实现SwitchButton,第二行则是完全绘制出来的SwitchButton效果。接下来我们分别一一讲述代码实现。<喎�"/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxociAvPg0KPGgyIGlkPQ=="2switchbutton样式两种实现">2.SwitchButton样式两种实现
状态开关按钮常用于某些控制开关,设置选项里最为常见。
2.1 组合View实现
该方法比较简单明了,定义三个view,开启状态和关闭状态两个背景View,一个圆形按钮view。点击时候利用滑动动画移动按钮和状态背景,达到类似的视觉效果。
先看xml布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<code
class
=
"language-java"
hljs=
""
><!--?xml version=
1.0
encoding=utf-
8
?-->
<merge android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
xmlns:android=
"https://schemas.android.com/apk/res/android"
>
<framelayout android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
>
<imageview android:id=
"@+id/on_bg_view"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
android:src=
"@drawable/switch_on_bg"
>
<imageview android:id=
"@+id/off_bg_view"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
android:src=
"@drawable/switch_off_bg"
>
</imageview></imageview></framelayout>
<imageview android:id=
"@+id/circle_view"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
android:src=
"@drawable/switch_circle"
>
</imageview></merge></code>
|
因为是帧布局,所以顶层使用merge(merge简化xml不解释,自行百度)。然后使用两个开关状态背景和一个圆形按钮组合而成。
1. 全局变量参数
1
2
3
4
5
6
7
8
9
10
|
<code
class
=
"language-java"
hljs=
""
>
public
class
SwitchView
extends
FrameLayout {
protected
boolean
isChecked;
//是否选中状态
protected
View onBgView;
protected
View offBgView;
protected
View circleView;
protected
boolean
autoForPerformClick =
true
;
//是否允许点击自动切换
protected
OnCheckedChangedListener onCheckedChangedListener;
//切换事件监听
//...
}</code>
|
一般状态切换是由click事件监听,根据业务逻辑来判断是否切换状态。但对于switchButton,通常我们操作时直观感受应该是先切换了状态才执行相应操作的,所以我们在performClick事件中直接根据autoForPerformClick 的状态来相应点击操作。
至于performClick ,其实就是控制条用onClickListener的方法体,具体逻辑在View源码中查看。
2. 初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<code
class
=
"language-java"
hljs=
""
>
public
SwitchView(Context context) {
super
(context);
initialize();
}
public
SwitchView(Context context, AttributeSet attrs) {
super
(context, attrs);
initialize();
}
public
SwitchView(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
initialize();
}
protected
void
initialize() {
setClickable(
true
);
LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
layoutInflater.inflate(R.layout.switch_view,
this
);
onBgView = findViewById(R.id.on_bg_view);
offBgView = findViewById(R.id.off_bg_view);
circleView = findViewById(R.id.circle_view);
}</code>
|
3. 点击响应
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<code
class
=
"language-java"
hljs=
""
>
@Override
public
boolean
performClick() {
if
(!autoForPerformClick)
//如果不是自动响应则调用默认处理方法
return
super
.performClick();
/**
*否则直接切换switch状态并触发事件监听
*/
setChecked(!isChecked,
true
);
if
(onCheckedChangedListener !=
null
) {
onCheckedChangedListener.onChanged(
this
, isChecked);
}
return
super
.performClick();
}</code>
|
View点击后会执行performClick方法,并判断是否调用clickLisentener。这里我们直接重写performClick方法,如果自动响应autoForPerformClick为ture则直接切换Switch状态,否则调用默认处理逻辑。
4.切换状态动画
点击打开,则圆形按钮从左端滑动到右端,onBg显示,offBg隐藏;
再点击关闭,圆形按钮从右端滑动到左端,onBg隐藏,offBg显示。
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
|
<code
class
=
"language-java"
hljs=
""
>
public
void
setChecked(
boolean
value,
boolean
needAnimate) {
if
(isChecked == value)
return
;
isChecked = value;
float
targetX =
0
;
//要移动的目标位置
if
(getWidth() !=
0
) {
//当前view没有渲染上去时候,getWidth()为零
targetX = getWidth() - circleView.getWidth();
}
else
{
measure(
0
,
0
);
targetX = getMeasuredWidth() - circleView.getMeasuredWidth();
}
long
durationMillis = needAnimate ?
200
:
0
;
if
(isChecked) {
onBgView.bringToFront();
//显示在最前端
onBgView.setVisibility(View.VISIBLE);
offBgView.setVisibility(View.VISIBLE);
//平移动画
TranslateAnimation an1 =
new
TranslateAnimation(
0
, targetX,
0
,
0
);
an1.setFillAfter(
true
);
an1.setDuration(durationMillis);
circleView.startAnimation(an1);
//透明度动画
AlphaAnimation an2 =
new
AlphaAnimation(
0
,
1
);
an2.setFillAfter(
true
);
an2.setDuration(durationMillis);
onBgView.startAnimation(an2);
}
else
{
offBgView.bringToFront();
onBgView.setVisibility(View.VISIBLE);
offBgView.setVisibility(View.VISIBLE);
TranslateAnimation an1 =
new
TranslateAnimation(targetX,
0
,
0
,
0
);
an1.setFillAfter(
true
);
an1.setDuration(durationMillis);
circleView.startAnimation(an1);
AlphaAnimation an2 =
new
AlphaAnimation(
0
,
1
);
an2.setFillAfter(
true
);
an2.setDuration(durationMillis);
offBgView.startAnimation(an2);
}
}</code>
|
状态切换的两个参数,value是否打开状态,needAnimate是否需要动画(否则直接切换效果)。setFillAfter保留动画结束状态,但并不影响View本身位置和状态。切换时,先将当前显示背景移动到最前端,其次添加按钮动画和渐隐动画。
至此,最基本的组合View实现已经完成了。想要了解详情的请在源码中查看。源码分为两部分,一个项目是View的实现lib,另一块是示例演示demo.
2.2 自定义View绘制实现
由于该样式并不十分复杂,所以可以通过基本的图形绘制draw出同样的效果。
具体实现逻辑:通过自定view属性来确定按钮大小和中间圆钮大小,在测量onMesure方法中控制测量值mode和Size,并在onLayout方法中得到圆钮半径和起始点位置。然后进行绘制,先绘制底部on圆角矩形背景,再绘制off渐变缩放的圆角矩形,最后绘制spot圆钮。
嘴比较笨拙,又不会画图。用word的图形工具将就画下可以看就好了。
具体实现大体都类似,这里贴上主要部分代码
1.全局参数
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
|
<code
class
=
"language-java"
hljs=
""
>
public
class
SwitchButton
extends
View{
/** */
private
float
radius;
/** 开启颜色*/
private
int
onColor = Color.parseColor(#4ebb7f);
/** 关闭颜色*/
private
int
offBorderColor = Color.parseColor(#dadbda);
/** 灰色带颜色*/
private
int
offColor = Color.parseColor(#ffffff);
/** 手柄颜色*/
private
int
spotColor = Color.parseColor(#ffffff);
/** 边框颜色*/
private
int
borderColor = offBorderColor;
/** 画笔*/
private
Paint paint ;
/** 开关状态*/
private
boolean
toggleOn =
false
;
/** 边框大小*/
private
int
borderWidth =
2
;
/** 垂直中心*/
private
float
centerY;
/** 按钮的开始和结束位置*/
private
float
startX, endX;
/** 手柄X位置的最小和最大值*/
private
float
spotMinX, spotMaxX;
/**手柄大小 */
private
int
spotSize ;
/** 手柄X位置*/
private
float
spotX;
/** 关闭时内部灰色带高度*/
private
float
offLineWidth;
/** */
private
RectF rect =
new
RectF();
/** 默认使用动画*/
private
boolean
defaultAnimate =
true
;
private
OnSwitchChanged listener;
//...
}</code>
|
2.初始化与读取
读取自定义属性并赋值。讲了又讲的东西,略。
3.测量onMeasure与布局onLayout
在onMeasure方法中根据给定mode和size来限定View,如果高宽不为明确值(UNSPECIFIED/AT_MOST),则定义自身高宽为明确值。 关于MeasureSpec的详细讲解,这里附上爱哥的一篇文章–MeasureSpec,深入到赋值读取的内部,不妨试着深入研究下。当然,更直接的方法就是点开源码一探究竟咯。
onLayout方法中取得view的实际高宽,计算出圆角矩形半径,圆钮半径以及起始点x方向位置。还有On矩形和off矩形的宽度。
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
|
<code
class
=
"language-java"
hljs=
""
>
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
final
int
widthMode = MeasureSpec.getMode(widthMeasureSpec);
final
int
heightMode = MeasureSpec.getMode(heightMeasureSpec);
int
widthSize = MeasureSpec.getSize(widthMeasureSpec);
int
heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**
*如果高宽未指定,则使用内置高宽明确大小
*/
Resources r = Resources.getSystem();
if
(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){
widthSize = (
int
) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
50
, r.getDisplayMetrics());
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
}
if
(heightMode == MeasureSpec.UNSPECIFIED || heightSize == MeasureSpec.AT_MOST){
heightSize = (
int
) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
30
, r.getDisplayMetrics());
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
}
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected
void
onLayout(
boolean
changed,
int
left,
int
top,
int
right,
int
bottom) {
super
.onLayout(changed, left, top, right, bottom);
final
int
width = getWidth();
final
int
height = getHeight();
/**
*测量相应大小
*/
radius = Math.min(width, height) *
0
.5f;
centerY = radius;
startX = radius;
endX = width - radius;
spotMinX = startX + borderWidth;
spotMaxX = endX - borderWidth;
spotSize = height -
4
* borderWidth;
spotX = toggleOn ? spotMaxX : spotMinX;
offLineWidth =
0
;
}</code>
|
前三步完成基本赋值之后,开始设置和绑定相应事件。这里不作为重点部分也省略,主要讲一下绘制过程和核心控制逻辑。
4.绘制过程
按照前面的简易示例图来绘制我们的ui图。
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
|
<code
class
=
"language-java"
hljs=
""
>
@Override
public
void
draw(Canvas canvas) {
//绘制on背景
rect.set(
0
,
0
, getWidth(), getHeight());
paint.setColor(borderColor);
canvas.drawRoundRect(rect, radius, radius, paint);
//绘制off背景(缩放至0时候不绘制)
if
(offLineWidth >
0
){
final
float
cy = offLineWidth *
0
.5f;
rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy);
paint.setColor(offColor);
canvas.drawRoundRect(rect, cy, cy, paint);
}
//绘制圆钮轮廓border
rect.set(spotX -
1
- radius, centerY - radius, spotX +
1
.1f + radius, centerY + radius);
paint.setColor(borderColor);
canvas.drawRoundRect(rect, radius, radius, paint);
//绘制圆钮
final
float
spotR = spotSize *
0
.5f;
rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR);
paint.setColor(spotColor);
canvas.drawRoundRect(rect, spotR, spotR, paint);
}</code>
|
及诶按来便是我们的状态切换动画控制逻辑,即点击按钮之后setToggleOn或者setToggleOff执行的相应动作。
4.状态切换动画效果
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
58
59
60
61
62
63
64
65
66
|
<code
class
=
"language-java"
hljs=
""
>
/**
* 执行效果,如果animate为true表示有动画效果
* 否则直接执行计算并显示最终打开1或者关闭0的效果绘制
*/
private
void
takeEffect(
boolean
animate) {
if
(animate){
slide();
}
else
{
calculateEffect(toggleOn ?
1
:
0
);
}
}
/**
*这里偷个懒,直接使用空的animation,根据当前interpolatedTime(0~1)渐变过程来绘制不同阶段的View,达到动画效果
*当然,也可以开启个线程或者定时任务,来实现从0到1的变换,劲儿改变视图绘制过程
*/
private
void
slide(){
Animation animation =
new
Animation() {
@Override
protected
void
applyTransformation(
float
interpolatedTime,
Transformation t) {
if
(toggleOn){
calculateEffect(interpolatedTime);
}
else
{
calculateEffect(
1
-interpolatedTime);
}
}
};
animation.setDuration(
200
);
clearAnimation();
startAnimation(animation);
}
/**
*计算绘制位置
*mapValueFromRangeToRange方法计算从当前位置相对于目标位置所对应的值
*通过颜色变化来达到透明度动画效果(颜色渐变)
*/
private
void
calculateEffect(
final
double
value) {
final
float
mapToggleX = (
float
) mapValueFromRangeToRange(value,
0
,
1
, spotMinX, spotMaxX);
spotX = mapToggleX;
float
mapOffLineWidth = (
float
) mapValueFromRangeToRange(
1
- value,
0
,
1
,
10
, spotSize);
offLineWidth = mapOffLineWidth;
final
int
fb = Color.blue(onColor);
final
int
fr = Color.red(onColor);
final
int
fg = Color.green(onColor);
final
int
tb = Color.blue(offBorderColor);
final
int
tr = Color.red(offBorderColor);
final
int
tg = Color.green(offBorderColor);
int
sb = (
int
) mapValueFromRangeToRange(
1
- value,
0
,
1
, fb, tb);
int
sr = (
int
) mapValueFromRangeToRange(
1
- value,
0
,
1
, fr, tr);
int
sg = (
int
) mapValueFromRangeToRange(
1
- value,
0
,
1
, fg, tg);
sb = clamp(sb,
0
,
255
);
sr = clamp(sr,
0
,
255
);
sg = clamp(sg,
0
,
255
);
borderColor = Color.rgb(sr, sg, sb);
postInvalidate();
}</code>
|
以上就是自定义View绘制的核心代码,详细查看源码SwitchButton。相较于组合方法,它更便捷,也有更高的灵活性和扩展性。同时还不需要图片资源支持。
3.SegmentControl样式实现
常见的Tab有很多种,这里使用的是IOS常见的一种切换效果SegmentControl。本篇只用最简单的拼装View实现类似效果。有兴趣的可以自己尝试绘制达到更优效果。(有空的话也会在后边放出)
通过view组合生成 最近单的方案,没有之一。使用现成的selector和背景来控制显示效果。各个子view分别继承 RelativeLayout并实现OnClick接口。最后在Segment中控制显示和点击切换。 自定义View绘制生成 这里只是提供思路。定义一个ItemView,根据在Segment中位置挥之不同效果。背景效果会用selector.xml的都知道,使用shape标签产生的drawable对象,其实就是一个 GradientDrawable 。所以我们自定义view可以直接通过使用GradientDrawable的 setCornerRadii(float[] radii) 来绘制同样的背景效果,劲儿可以做到不同颜色。最后,使用一个ViewGroup不含这些item即可。通过click事件来切换tab就可以了。3.1 组合View实现
首先,类似的定义一个可点击的通用的RelativLayout。(实现 Checkable接口使其可被选中也移除选中状态,详细可以参考前面的博文 微博/动态 点赞效果)。这里涉及三个新内容,稍微说明讲解下。
- checkMode 选中模式,是单选 CHECKMODE_CHECK 还是 CHECKMODE_RADIO 单选效果。使我们的自定义RelativeLayout可以做到单选和复选。
- onInitializeAccessibilityEvent 添加View接受事件源信息。即订阅checked事件。由于事件可能由内部子view点击触发,所以这里应该接收并处理相应的checked事件。当然,使用该方法首先要重写onInitializeAccessibilityNodeInfo方法,添加我们关注的状态信息。
- SavedState状态保存 当我们内部可能嵌套复杂view的时候,为了防止数据状态丢失,一般需要定义状态保存类,用以保存和恢复当前View状态。
#### 1.可点击的通用RelativeLayout
继承实现Clickable接口 ,简要略过。
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
|
<code
class
=
"language-java"
hljs=
""
>
//定义checked状态
public
static
final
int
[] CHECKED_STATE_SET = { android.R.attr.state_checked };
//重写SetChecked方法和isChecked方法略
/**
*根据当前选择模式checkMode 来控制单复选
*/
@Override
public
boolean
performClick() {
if
(checkMode == CHECKMODE_CHECK) {
toggle();
}
else
if
(checkMode == CHECKMODE_RADIO) {
setChecked(
true
);
}
return
super
.performClick();
}
/**
*添加Drawable 的checked状态 ,并再绘制view是绘制相应状态效果
*/
@Override
public
int
[] onCreateDrawableState(
int
extraSpace) {
int
[] states =
super
.onCreateDrawableState(extraSpace +
1
);
if
(isChecked()) {
mergeDrawableStates(states, CHECKED_STATE_SET);
}
return
states;
}
@Override
protected
void
drawableStateChanged() {
super
.drawableStateChanged();
Drawable drawable = getBackground();
if
(drawable !=
null
) {
int
[] myDrawableState = getDrawableState();
drawable.setState(myDrawableState);
invalidate();
}
}</code>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<code
class
=
"language-java"
hljs=
""
>
@Override
public
void
onInitializeAccessibilityEvent(AccessibilityEvent event) {
super
.onInitializeAccessibilityEvent(event);
event.setClassName(CheckedRelativeLayout.
class
.getName());
event.setChecked(checked);
}
@Override
public
void
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super
.onInitializeAccessibilityNodeInfo(info);
info.setClassName(CheckedRelativeLayout.
class
.getName());
info.setCheckable(
true
);
info.setChecked(checked);
}</code>
|
View自身重写保存和恢复的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<code
class
=
"language-java"
hljs=
""
>
@Override
public
Parcelable onSaveInstanceState() {
//保存
Parcelable superState =
super
.onSaveInstanceState();
SavedState ss =
new
SavedState(superState);
ss.checked = isChecked();
return
ss;
}
@Override
public
void
onRestoreInstanceState(Parcelable state) {
//恢复
SavedState ss = (SavedState) state;
super
.onRestoreInstanceState(ss.getSuperState());
setChecked(ss.checked);
requestLayout();
}</code>
|
用于保存数据的基本状态类型
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
|
<code
class
=
"language-java"
hljs=
""
>
static
class
SavedState
extends
BaseSavedState {
boolean
checked;
SavedState(Parcelable superState) {
super
(superState);
}
private
SavedState(Parcel in) {
super
(in);
checked = (Boolean) in.readValue(
null
);
}
@Override
public
void
writeToParcel(Parcel out,
int
flags) {
super
.writeToParcel(out, flags);
out.writeValue(checked);
}
public
static
final
Creator<savedstate> CREATOR =
new
Creator<savedstate>() {
public
SavedState createFromParcel(Parcel in) {
return
new
SavedState(in);
}
public
SavedState[] newArray(
int
size) {
return
new
SavedState[size];
}
};
}
@Override
public
String toString() {
return
CompoundButton.SavedState{ + Integer.toHexString(System.identityHashCode(
this
)) + checked= + checked + };
}</savedstate></savedstate></code>
|
2.控制tab切换的SegmentView
代码比较易于理解,这里直接贴出来查阅即可。
基本思路,水平线性布局包裹对应左中右不同item个数的选项,并通过设置对应left/right/center来设置背景。然后分别为每个Item设置同一个点击事件,点击之后检查是否当前item被选中,改变statu,同时出发切换事件。详细代码:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
|
<code
class
=
"language-java"
hljs=
""
>
public
class
SegmentView
extends
LinearLayout {
protected
final
static
int
SEGMENT_LEFT_BG = R.drawable.segment_left_selector;
protected
final
static
int
SEGMENT_CENTER_BG = R.drawable.segment_center_selector;
protected
final
static
int
SEGMENT_RIGHT_BG = R.drawable.segment_right_selector;
protected
int
leftBg = SEGMENT_LEFT_BG;
protected
int
centerBg = SEGMENT_CENTER_BG;
protected
int
rightBg = SEGMENT_RIGHT_BG;
protected
CheckedRelativeLayout2[] checkedRelativeLayouts;
protected
int
index = -
1
;
protected
float
textSize = -
1
;
protected
int
textColorN = Color.BLACK, textColorP = Color.BLACK;
protected
OnIndexChangedListener onIndexChangedListener;
public
SegmentView(Context context) {
super
(context);
initialize();
}
public
SegmentView(Context context, AttributeSet attrs) {
super
(context, attrs);
initialize();
initFromAttributes(context, attrs);
}
public
SegmentView(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
initialize();
initFromAttributes(context, attrs);
}
protected
void
initialize() {
setGravity(Gravity.CENTER);
}
protected
void
initFromAttributes(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SegmentView);
String content = a.getString(R.styleable.SegmentView_content);
index = a.getInt(R.styleable.SegmentView_index, index);
textSize = a.getDimension(R.styleable.SegmentView_textSize, textSize);
textColorN = a.getColor(R.styleable.SegmentView_textColorN, textColorN);
textColorP = a.getColor(R.styleable.SegmentView_textColorP, textColorP);
leftBg = a.getResourceId(R.styleable.SegmentView_leftBg, leftBg);
centerBg = a.getResourceId(R.styleable.SegmentView_centerBg, centerBg);
rightBg = a.getResourceId(R.styleable.SegmentView_rightBg, rightBg);
a.recycle();
if
(!TextUtils.isEmpty(content)) {
String[] contentStrings = content.split(,);
setContent(contentStrings);
}
setIndex(index);
}
public
void
setContent(String... content) {
View[] views =
new
View[content.length];
for
(
int
i =
0
, len = content.length; i < len; i++) {
String s = content[i];
TextView tv =
new
TextView(getContext());
tv.setTextColor(textColorN);
tv.setText(s);
if
(textSize != -
1
) {
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
views[i] = tv;
}
setContent(views);
}
public
void
setContent(View... content) {
removeAllViews();
int
lastIndex = content.length -
1
;
checkedRelativeLayouts =
new
CheckedRelativeLayout2[content.length];
checkedRelativeLayouts[
0
] = createLeftView(content[
0
]);
checkedRelativeLayouts[lastIndex] = createRightView(content[lastIndex]);
for
(
int
i =
1
; i < lastIndex; i++) {
checkedRelativeLayouts[i] = createCenterView(content[i]);
}
for
(View view : checkedRelativeLayouts) {
LayoutParams llp =
new
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
llp.weight =
1
;
addView(view, llp);
}
}
public
int
getIndex() {
return
index;
}
public
void
setIndex(
int
i) {
if
(i <
0
)
return
;
checkedRelativeLayouts[i].setChecked(
true
);
}
public
void
setTextColorN(
int
textColorN) {
this
.textColorN = textColorN;
}
public
void
setTextColorP(
int
textColorP) {
this
.textColorP = textColorP;
}
protected
CheckedRelativeLayout.OnCheckedChangeListener checkedChangeListener =
new
CheckedRelativeLayout.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CheckedRelativeLayout layout,
boolean
isChecked) {
if
(isChecked) {
for
(CheckedRelativeLayout2 item : checkedRelativeLayouts) {
if
(!item.equals(layout)) {
item.setChecked(
false
);
}
}
if
(onIndexChangedListener !=
null
) {
int
i = indexOf(checkedRelativeLayouts, layout);
index = i;
if
(onIndexChangedListener !=
null
) {
onIndexChangedListener.onChanged(SegmentView.
this
, index);
}
}
}
}
};
protected
CheckedRelativeLayout2 createLeftView(View contentView) {
CheckedRelativeLayout2 layout =
new
CheckedRelativeLayout2(getContext());
layout.setBackgroundResource(leftBg);
layout.setGravity(Gravity.CENTER);
layout.addView(contentView);
layout.setOnCheckedChangeListener(checkedChangeListener);
return
layout;
}
protected
CheckedRelativeLayout2 createCenterView(View contentView) {
CheckedRelativeLayout2 layout =
new
CheckedRelativeLayout2(getContext());
layout.setBackgroundResource(centerBg);
layout.setGravity(Gravity.CENTER);
layout.addView(contentView);
layout.setOnCheckedChangeListener(checkedChangeListener);
return
layout;
}
protected
CheckedRelativeLayout2 createRightView(View contentView) {
CheckedRelativeLayout2 layout =
new
CheckedRelativeLayout2(getContext());
layout.setBackgroundResource(rightBg);
layout.setGravity(Gravity.CENTER);
layout.addView(contentView);
layout.setOnCheckedChangeListener(checkedChangeListener);
return
layout;
}
public
void
setOnIndexChangedListener(OnIndexChangedListener l) {
this
.onIndexChangedListener = l;
}
protected
class
CheckedRelativeLayout2
extends
CheckedRelativeLayout {
protected
TextView textView;
public
CheckedRelativeLayout2(Context context) {
super
(context);
}
@Override
public
void
addView(View child) {
super
.addView(child);
if
(child
instanceof
TextView) {
textView = (TextView) child;
}
}
@Override
public
void
setChecked(
boolean
checked) {
super
.setChecked(checked);
if
(textView !=
null
) {
if
(checked) {
textView.setTextColor(textColorP);
}
else
{
textView.setTextColor(textColorN);
}
}
}
}
public
static
interface
OnIndexChangedListener {
public
void
onChanged(SegmentView view,
int
index);
}
public
static
<t>
int
indexOf(T[] array, T obj) {
for
(
int
i =
0
, len = array.length; i < len; i++) {
if
(array[i].equals(obj))
return
i;
}
return
-
1
;
}
}</t></code>
|
该方法比较简陋,背景颜色定制性不高。即只能通过既定drawable北京来实现。不过,其实是可以通过selector来定义相关背景drawable的。不妨试一下。
3.2 自定义View实现
本来此方法只是简单提及的一个想法而已,今天有空就一并写了。时间匆忙,代码稍微有些混乱,不过还是能起到一定示范效用的,这里也贴出来供大家参考。
整体思路:
定义子item 设置其选中状态和字体/背景色。通过测量方法保证显示范围和字体大小,通过GradientDrawable绘制圆角背景,并画对应字体。
定义Segment 继承自ViewGroup,读取自定义属性,根据文本内容添加子View。然后重写OnMeasure方法和OnLayout方法来测量和布局子View。最后添加点击事件,提供监听接口。
代码如下:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
<code
class
=
"language-java"
hljs=
""
>
import
com.qiao.demo.R;
import
com.qiao.demo.R.styleable;
import
android.content.Context;
import
android.content.res.TypedArray;
import
android.graphics.Canvas;
import
android.graphics.Color;
import
android.graphics.Paint;
import
android.graphics.Rect;
import
android.graphics.drawable.GradientDrawable;
import
android.util.AttributeSet;
import
android.util.TypedValue;
import
android.view.View;
import
android.view.ViewGroup;
import
android.view.View.OnClickListener;
public
class
SegmentView
extends
ViewGroup
implements
OnClickListener{
private
final
float
r = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
4
, getResources().getDisplayMetrics());
private
int
bgColor =
0xff0072c6
;
private
int
fgColor = Color.WHITE;
private
float
mTextSize = 3f*r;
private
String []mText= {item1,item2,item3};
private
int
checkedItem=
1
;
private
OnItemClickListener listener;
public
SegmentView(Context context) {
super
(context);
initFromAttributes(context,
null
);
initalize();
}
public
SegmentView(Context context, AttributeSet attrs) {
super
(context, attrs);
initFromAttributes(context,attrs);
initalize();
}
protected
void
initFromAttributes(Context context, AttributeSet attrs) {
if
(attrs==
null
)
return
;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SegmentView0);
String content = a.getString(R.styleable.SegmentView0_content0);
if
(!isEmpty(content)){
mText = content.split(,);
}
checkedItem = a.getInt(R.styleable.SegmentView0_index0, checkedItem);
mTextSize = a.getDimension(R.styleable.SegmentView0_textSize0, mTextSize);
bgColor = a.getColor(R.styleable.SegmentView0_bgColor, bgColor);
fgColor = a.getColor(R.styleable.SegmentView0_textColor, fgColor);
a.recycle();
}
public
void
initalize(){
int
length = mText.length;
for
(
int
i=
0
;i<length;i++){ childwidthmeasurespec=
"widthMeasureSpec;"
count=
"getChildCount();"
i=
"=checkedItem);"
int
=
""
maxheight=
"0;"
maxwidth=
"0;"
override=
""
protected
=
""
view=
""
void
=
""
widthmode=
"MeasureSpec.getMode(widthMeasureSpec);"
widthsize=
""
>=
0
){
maxWidth = widthSize/count;
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth,widthMode);
}
for
(
int
i=
0
;i<count;i++){
boolean
=
""
case
=
""
checkeditem=
"i;"
child=
"getChildAt(i);"
context=
""
count=
"getChildCount();"
drawable=
"new"
else
=
""
extends
=
""
final
=
""
gradientdrawable=
""
gravity=
"GRAVITY_SINGLE;"
gravity_center=
"1<<2;"
gravity_center:=
""
gravity_left=
"1<<1;"
gravity_left:=
""
gravity_right=
"1<<3;"
gravity_right:=
""
gravity_single=
"1<<0;"
gravity_single:=
""
height=
""
heightmeasurespec=
"MeasureSpec.makeMeasureSpec(heightSize,heightMode);"
heightmode=
"="
heightsize=
"MeasureSpec.getSize(heightMeasureSpec);"
i=
"=0){"
int
=
""
item=
""
itemview=
""
left=
"0;"
maxheight=
"Math.max(maxHeight,child.getMeasuredHeight());"
maxwidth=
"Math.max(maxWidth,child.getMeasuredWidth());"
mtextbound=
"new"
mtextpaint=
"new"
new
=
""
override=
""
paint=
""
private
=
""
protected
=
""
public
=
""
r=
""
rect=
""
return
=
""
static
=
""
string=
""
switch
=
""
this
.gravity=
"gravity;"
this
.ischecked=
"isChecked;"
this
.text=
"text;"
view=
""
void
=
""
width=
"getMeasuredWidth();"
widthmeasurespec=
"MeasureSpec.makeMeasureSpec(widthSize,widthMode);"
widthmode=
"="
widthsize=
"MeasureSpec.getSize(widthMeasureSpec);"
>=
0
){
float
textSize = Math.min(mTextSize,height-
2
*r);
if
(width>
0
){
textSize = Math.min(textSize,(width-
2
*r)*
2
/text.length());
//英文比中文短(中文为两个字符),故取mText.length()/2作为平均宽度
}
if
(textSize != mTextSize ){
mTextPaint.setTextSize(textSize);
mTextPaint.getTextBounds(text,
0
, text.length(), mTextBound);
}
}
}
@Override
public
void
draw(Canvas canvas) {
Rect rect = canvas.getClipBounds();
drawable.setBounds(
new
Rect(rect));
drawable.draw(canvas);
int
l = (rect.width() - mTextBound.width())/
2
;
int
b = (rect.height() + mTextBound.height())/
2
;
canvas.drawText(text, l, b, mTextPaint);
}
}
public
void
setOnItemClickListener(OnItemClickListener onItemClickListener){
this
.listener = onItemClickListener;
}
interface
OnItemClickListener{
void
onItemClick(ItemView item,
int
checkedItem);
}
public
static
boolean
isEmpty(String str){
return
null
==str || str.trim().length() ==
0
;
}
}
</count;i++){></length;i++){></code>
|
参照前面两段讲述完全可以理解了。使用时候可以方便的通过自定义属性来控制字体颜色和点击背景。可以动态变更View高宽。有问题的同学可以在文末提出或指正。
3.总结
感觉自己学习进步的速度很慢,常常伴随着焦急浮躁。这篇文章也是积累了好久才慢吞吞的写完了。代码方面,个人也有不少不良习惯,助事业不够清晰,不过总体上不是有碍观瞻吧。
同样的东西,尝试用不同想法写两遍,我觉得是有好处的。至少于我,能看到不少有意思的东西。
最后, 附上本文的 示例源码 . 由于资源上传较早,第二部分的自定义View并没有打包上传。不过上便已经贴出完整代码了,可以直接拿来使用。
后边在考虑是写一写非UI层面的东西,还是继续写关于常见的增删改UI界面。待定,总之,fighting..