仿QQ聊天界面文字过长显示

前言

最近一直在做聊天功能,有群聊,有单聊,没有集成第三方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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值