仿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。