大家好,今天是大年初七,相信很多人都已经上班了,很不好意思,我还在放假中(自己脑补表情)。
最近也没什么事,就突然想起了当年面试的时候,面试官问我有没有写过自定义标签组控件,并叫我讲一下如何实现的。当时没做过啊,但肯定不能说没做过,然后立马在脑中开始思考如何实现该控件。最后回答的马马虎虎,面试官人好,也没深问,要不肯定傻了。
今天,自己实现了这个自定义控件,思路还是很简单的,就和大家分享一下我的思路,如果有其他思路可以一起讨论。
下面先放一张某软件的标签截图因为这篇文章主要讲思路,所以界面大家看看就好。
下面开始正题 下面开始正题 下面开始正题
我先给大家看一张图(来自灵魂画手)图中我们分别用1,2,3,4,5来表示不用的标签,假设标签的长度不一样,高度一样。每两个标签之间会有相同的间隔距离,上下之间没有间隔距离。我们要做的就是将标签放在同一排,如果放不下,就从下一行开始摆放。
控件在ViewGroup中设置位置用到的方法是
public void layout(int l, int t, int r, int b)
方法中四个参数分别是控件在父控件中左,上,右,下的坐标。
现在我们开始分析每一个标签的位置如何摆放:
1:第一个标签如果没有特殊要,参数可以为( 0, 0, 标签1宽, 标签1高 )
2:第二个标签放置的时候我们需要判断第一行是否还放得下第二个标签。如果可以,第二个标签的参数为( 标签1宽+间隔距离, 0, 标签1宽+间隔距离+标签2宽, 标签2高)。如果不能,那么参数应该为(0, 标签1高, 标签2宽, 标签1高+标签2高)。
3:其他的标签都按照这个规则来摆放。如果没有看懂的,结合灵魂画手图片应该可以理解。
因为自定义的控件是标签组,所以会继承ViewGroup,并重写onMeasure以及onLayout方法来放置标签控件。这里用TextView来代替标签控件,大家也可以用其他的控件。(这里涉及到View绘制流程的一些知识,如果不懂得可以先看看View的绘制流程,网上有很多资源)
首先我们来看看添加标签控件的代码:
//加入标签
public void setLabs(List labs) {
//重置数据
removeAllViews();
layoutLeft = 0;
layoutTop = 0;
for (final String lab : labs) {
final TextView textView = new TextView(getContext());
textView.setText(lab);
textView.setTextColor(Color.WHITE);
//设置标签点击事件
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickLabCallBack != null) {
clickLabCallBack.clickLabCallBack(lab);
}
}
});
//添加标签控件
addView(textView);
}
}
layoutLeft:标签距离父控件左边的距离
layoutTop:标签距离父控件上部的距离。
final TextView textView = new TextView(getContext());
textView.setText(lab);
textView.setTextColor(Color.WHITE);
...
addView(textView);
这里的代码很简单,通过循环将每一个新建的TextView赋值,并将TextView添加到父控件中。
//设置标签点击事件
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickLabCallBack != null) {
clickLabCallBack.clickLabCallBack(lab);
}
}
});
设置点击事件。
接下来看看测量标签高宽的代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
measureChild(view, widthMeasureSpec, heightMeasureSpec);
}
}
这里是重写了onMeasure方法,然后在方法中通过measureChild方法对子View的高宽进行了测量,如果这里不进行测量,后面用到控件高宽的时候就会为0。
下面来看看最重要的代码:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
//判断是否需要换行
if (layoutLeft + view.getMeasuredWidth() > screenWidth) {
//换行
layoutLeft = 0;
layoutTop += view.getMeasuredHeight();
}
view.layout(layoutLeft, layoutTop, layoutLeft + view.getMeasuredWidth(), layoutTop + view.getMeasuredHeight());
//计算左侧距离
layoutLeft += (view.getMeasuredWidth() + 50);
}
}
放置控件的代码放在了onLayout方法中,ViewGroup的这个方法是一个空实现,就是方便让不同的ViewGroup定制自己的规则。
我们这里用到了两个参数,layoutLeft和layoutTop。layoutLeft用来记录最后一个标签距离父控件左边的距离,layoutTop则是记录最后一个标签距离父控件顶部的距离。
layoutLeft += (view.getMeasuredWidth() + 50);
每放置一个标签,layoutLeft就会加上放置的这个标签的宽和它右边的间隔距离(间距默认为50像素)。
layoutLeft + view.getMeasuredWidth() > screenWidth
我们通过这个方法来判断是否需要换行。screenWidth为手机屏幕宽度,我们这里假设控件宽为屏幕宽。
if (layoutLeft + view.getMeasuredWidth() > screenWidth) {
//换行
layoutLeft = 0;
layoutTop += view.getMeasuredHeight();
}
如果换行layoutLeft就变成0。layoutTop就会加上标签的高和间距(Demo中高度间距为0,所以没加),如果换了两行,那么layoutTop的值就应该是两个标签高加上两个间距的距离。
view.layout(layoutLeft, layoutTop, layoutLeft + view.getMeasuredWidth(), layoutTop + view.getMeasuredHeight());
通过layout方法来设置控件位置,layout后两个参数我是用该View在父控件中左边和上面的坐标分别加上View本身的宽和高,这样刚好位置大小能放下这个View。
到这里所有的代码就分析完毕,大家加上自己的思考理解这些代码应该问题不大。如果有没有讲清楚的地方欢迎大家来提问。如果有错误的地方也欢迎大家指正出来。最后献上完整代码。
xml:
控件代码:
public class LabView extends ViewGroup {
private int layoutLeft;
private int layoutTop;
private int screenWidth;
public LabView(Context context) {
this(context, null);
}
public LabView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LabView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Point sizePoint = new Point();
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getSize(sizePoint);
screenWidth = sizePoint.x;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
measureChild(view, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
//判断是否需要换行
if (layoutLeft + view.getMeasuredWidth() > screenWidth) {
//换行
layoutLeft = 0;
layoutTop += view.getMeasuredHeight();
}
view.layout(layoutLeft, layoutTop, layoutLeft + view.getMeasuredWidth(), layoutTop + view.getMeasuredHeight());
//计算左侧距离
layoutLeft += (view.getMeasuredWidth() + 50);
}
}
//加入标签
public void setLabs(List labs) {
//重置数据
removeAllViews();
layoutLeft = 0;
layoutTop = 0;
for (final String lab : labs) {
final TextView textView = new TextView(getContext());
textView.setText(lab);
textView.setTextColor(Color.WHITE);
//设置标签点击事件
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickLabCallBack != null) {
clickLabCallBack.clickLabCallBack(lab);
}
}
});
//添加标签控件
addView(textView);
}
}
private ClickLabCallBack clickLabCallBack;
public interface ClickLabCallBack {
void clickLabCallBack(String content);
}
public void setClickLabCallBack(ClickLabCallBack clickLabCallBack) {
this.clickLabCallBack = clickLabCallBack;
}
}
MainActivity代码:
public class MainActivity extends AppCompatActivity {
private LabView labView;
private List list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
labView = findViewById(R.id.labview);
//初始化数据
initData();
findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//添加标签列表
labView.setLabs(list);
}
});
labView.setClickLabCallBack(new LabView.ClickLabCallBack() {
@Override
public void clickLabCallBack(String content) {
//设置列表点击事件
Toast.makeText(MainActivity.this, content, Toast.LENGTH_SHORT).show();
}
});
}
private void initData() {
list = new ArrayList<>();
list.add("李连杰");
list.add("杰");
list.add("杰森斯坦森");
list.add("巨石强森");
list.add("史泰龙");
list.add("阿诺斯瓦辛格");
list.add("杰");
list.add("杰森斯坦森");
list.add("巨石强森");
list.add("史泰龙");
list.add("阿诺斯瓦辛格");
list.add("李连杰");
list.add("杰森斯坦森");
list.add("巨石强森");
list.add("杰");
list.add("史泰龙");
list.add("阿诺斯瓦辛格");
}
}