前言
最近一直在做聊天功能,有群聊,有单聊,没有集成第三方SDK(例如环信)。从收到消息推送、插入数据库、到界面显示全是我们自己做的,在这个过程中碰到了很多问题,例如消息同步、前后台切换、界面刷新频率、收到上报等很多细节问题。
在做聊天列表界面时,如果跟你聊天的这个人是陌生人,需要在用户名字后面加一个陌生人的标签。这个标签必须要跟在名字的后面,这种情况用LinearLayout或者RelativeLayout布局都能实现,问题来了,如果名字过长的话,名字会占满一行,陌生人标签干脆不显示。
在聊天详细界面也碰到了同样的问题,如果某条聊天内容过长,并且这条消息还发送失败的话,需要在消息的左边显示重发按钮。
这两个问题其实就是一个问题,只是界面不一样而已,仔细想了想SDK为我们提供的几种常用局部,发现都不能实现我需要的效果。于是就只能通过自定义ViewGroup实现了。
先看效果图:
自定义ViewGroup步骤
- 最少需要重写两个构造方法
- 一般都需要重写两个方法,onMeasure(测量自己跟子View的宽高)跟onLayout(确定子View显示位置)
- 如果需要处理子View的边距等,需要重写generateLayoutParams方法。
上代码
因为需要判断是左边还是右边,所以得自定义属性,新建attrs.xml文件,增加如下代码,attr有两个值,left跟right用来决定左边还是右边:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="sigleLine">
<attr name="gravity">
<flag name="left" value="1"/>
<flag name="right" value="2"/>
</attr>
</declare-styleable>
</resources>
新建MySingleLineLayout类,继承自ViewGroup,重写两个构造方法,第一个构造方法调用this,就是调用第二个,然后在第二个构造方法中获取自定义属性的值:
public class MySingleLineLayout extends ViewGroup {
public static final int LEFT = 1;
public static final int RIGHT = 2;
private int gravity;
public MySingleLineLayout(Context context) {
this(context,null);
}
public MySingleLineLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//获取自定义属性的值
TypedArray typedArray=context.obtainStyledAttributes(attrs, R.styleable.sigleLine);
gravity=typedArray.getInt(R.styleable.sigleLine_gravity,0);
typedArray.recycle();
}
}
因为我们支持外边距,所以这里重写了generateLayoutParams方法,这里直接返回系统SDK里面的MarginLayoutParams对象,如果你想支持更多的属性,也可以自定义,只要继承ViewGroup.LayoutParams类就可以的:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
接下来重写onMeasure方法,测量自己的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);//获取ViewGroup的宽度
Log.i("ansen","gravity:"+gravity);
//未指定模式 父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
int unspecifiedMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
int firstWidth=width;
View firstView=null;
if(gravity == LEFT){
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
Log.i("ansen","i:"+i);
if(i==0){
firstView=child;
firstWidth-=(params.leftMargin+params.rightMargin);
}else{
if(child.getVisibility()!=View.GONE){//必须是占用空间的View
child.measure(unspecifiedMeasureSpec,unspecifiedMeasureSpec);
firstWidth -= (child.getMeasuredWidth()+getPaddingLeft()+getPaddingRight()+params.leftMargin+params.rightMargin);//第一个View可以显示的最大宽度
}
}
}
}else{
for(int i=getChildCount()-1;i>=0;i--){
View child=getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
Log.i("ansen","i:"+i);
if(i==getChildCount()-1){
firstView=child;
firstWidth-=(params.leftMargin+params.rightMargin);
}else{
if(child.getVisibility()!=View.GONE){//必须是占用空间的View
child.measure(unspecifiedMeasureSpec,unspecifiedMeasureSpec);
firstWidth -= (child.getMeasuredWidth()+getPaddingLeft()+getPaddingRight()+params.leftMargin+params.rightMargin);//第一个View可以显示的最大宽度
}
}
}
}
Log.i("ansen","maxWidth:"+firstWidth);
int maxWidthMeasureSpec = MeasureSpec.makeMeasureSpec(firstWidth, MeasureSpec.AT_MOST);
firstView.measure(maxWidthMeasureSpec,unspecifiedMeasureSpec);
int height = getPaddingBottom() + getPaddingTop() + firstView.getMeasuredHeight();
Log.i("ansen","width:"+width+" height:"+height);
setMeasuredDimension(width,height);
}
首先通过MeasureSpec.getSize方法获取当前ViewGroup的宽度。然后通过MeasureSpec.makeMeasureSpec方法生成一个不指定大小的模式。
方法内第6行代码,用if判断显示方向,是左边还是右边,如果是左边,那第一个View肯定是长度根据内容变化的View,所以需要ViewGroup宽度减掉后面所有View的宽度。当然还要减掉左右外边距。
方法内23行代码,如果是右边排序,进入else,右边恰恰相反,最后一个View肯定是长度根据内容变化的View,所以需要ViewGroup宽度减掉前面所有View的宽度,同时也要处理左右边距。
方法内41行代码,这个时候我们拿到了内容变化的View最大能显示的宽度,通过MeasureSpec.makeMeasureSpec方法生成宽度模式,这里需要注意的是这个方法的第二个参数MeasureSpec.AT_MOST,这个模式的意思是父容器指定了一个大小,即SpecSize,子view的大小不能超过这个SpecSize的大小。
方法内42行代码,调用firstView.measure方法,传入两个参数,指定大小模式,未定义模式。
子View都测量完成了,最后调用setMeasuredDimension方法,来决定ViewGroup自己的宽高。
重写onMeasure方法确定了宽高之后,就要决定子View显示的位置了,所以还需要重写onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i("ansen","onLayout gravity:"+gravity);
int firstHeight=0;//第一个View的高度
if(gravity==LEFT){//左边
int left=0;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
if(i==0){
firstHeight=child.getMeasuredHeight();
child.layout(getPaddingLeft()+params.leftMargin,getPaddingTop(),child.getMeasuredWidth()+params.leftMargin+params.rightMargin,getPaddingTop()+child.getMeasuredHeight());
}else{
int top=(firstHeight-child.getMeasuredHeight())/2;
child.layout(left+params.leftMargin,getPaddingTop()+params.topMargin+top,left+child.getMeasuredWidth()+params.leftMargin,getPaddingTop()+child.getMeasuredHeight()+params.topMargin+top);
}
left+=child.getMeasuredWidth() + getPaddingLeft()+params.leftMargin+params.rightMargin;
}
}else{//右边
int right=0;
for(int i=getChildCount()-1;i>=0;i--){
View child=getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
if(i==getChildCount()-1){
firstHeight=child.getMeasuredHeight();
child.layout(getWidth()-(getPaddingLeft()+params.leftMargin+child.getMeasuredWidth()),getPaddingTop(),getWidth()+params.rightMargin,getPaddingTop()+child.getMeasuredHeight());
}else{
Log.i("ansen","left:"+(getWidth()-right-child.getMeasuredWidth()-params.leftMargin)+" right:"+(getWidth()-right+params.rightMargin)+"child.getWidth():"+child.getMeasuredWidth());
int top=(firstHeight-child.getMeasuredHeight())/2;
child.layout(getWidth()-right-child.getMeasuredWidth()-params.rightMargin,getPaddingTop()+params.topMargin+top,getWidth()-right-params.rightMargin,getPaddingTop()+child.getMeasuredHeight()+params.topMargin+top);
}
right+=child.getMeasuredWidth()+params.leftMargin+params.rightMargin+getPaddingLeft()+getPaddingRight();
}
}
}
别看这个方法代码多,其实核心都在child.layout这句代码上,这个方法有四个参数,分别是left,top,right,bottom,这四个参数分别是View的四个点的坐标,这个坐标不是相对于屏幕左上角开始的,而是相对于ViewGroup开始的。
所以如果是左边开始显示的话,第一个View的layout方法四个值应该是:(0,0,测量宽度,测量高度),第二个View的值就是:(第一个View的宽度,0,第一个View的宽度+第二个View的宽度,测量高度)。
如果是右边显示的话,第一个View的layout方法四个值应该是:(ViewGroup宽度-自己的测量宽度,0,屏幕宽度,测量高度)。第二个View的值就是:(屏幕宽度-第一个View的宽度-第二个View的宽度,0,屏幕宽度-第一个View的宽度,测量高度)。
上面说的两种layout方法的四个值是没涉及到外边距跟内边距的情况下,只是为了方便大家理解。还有我们这里第二个View的高度并不是0至测量高度,因为第一个View的内容有可能显示两行,所以第二View需要垂直居中,这个时候top跟bottom的值就需要动态计算。
以上就是这个自定义ViewGroup的所有内容了,当你碰到类似的需求直接拿过去用就好了,当然如果你碰到相似的需求,通过本篇文章的学习,希望你也能搞定自定义ViewGroup。
更多Android进阶技术,面试资料系统整理分享,职业生涯规划,产品,思维,行业观察,谈天说地。可以加Android架构师群;701740775。