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

作者 : 开心源码 本文共6307个字,预计阅读时间需要16分钟 发布时间: 2022-05-12 共158人阅读

前言

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

@Overridepublic LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(),attrs);}

接下来重写onMeasure方法,测量自己的宽高。

@Overrideprotected 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方法。

@Overrideprotected 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。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 仿QQ聊天界面文字过长显示

发表回复