zoukankan      html  css  js  c++  java
  • Android 自绘TextView解决提前换行问题,支持图文混排

    先看下效果图:

    上面是MTextView,下面是默认的TextView。

    一、原因

    用最简单的全英文句子为例,如果有一个很长的单词,这一行剩余的空间显示不下了,那么规则就是不打断单词,而是把整个单词丢到下一行开始显示。这样 本来没有错。一是咱们中国人都是方块字,怎么都放得下,不存在英文的这个问题。所以不习惯那个排版。二是如果TextView里面有图片,如图,不知道判断单词的代码是怎么弄得,总之它觉得最后一个啦字和后面的一串表情应该是一个整体,不能分开,就一起丢到第二行了,也就造成了这种难看的排版。要验证这个说法也很简单,自己去QQ里试一试,在每个表情之间都加一个空格,就会发现排版一下子正常了。

    二、解决方法

        最简单的就是表情之间加空格,如果不想这么做,就只有自己来画啦。

        先给初学的朋友解释一下View绘制的流程,首先是onMeasure(int widthMeasureSpec, int heightMeasureSpec),onMeasure执行的时候,就是父View在问你,小朋友,你要占多大的地儿呀?当然,问你的时候,会给你个 限制条件,就是那两参数,以widthMeasureSpec为例,这参数不能直接用,得先拆开,用int widthMode = MeasureSpec.getMode(widthMeasureSpec) 和 int widthSize = MeasureSpec.getSize(widthMeasureSpec);widthMode就三种情况:

    MeasureSpec.EXACTLY:你就widthSize那么宽就行了。

    MeasureSpec.AT_MOST:你最多只能widthSize那么宽。

    MeasureSpec.UNSPECIFIED:未指定,你爱多宽多宽。

    当然,其实这只父View给你的建议,遵不遵守你自己看着办,但是自己乱来导致显示不全就不是父View的错了。

    最终你听取了建议,思量了一番,觉得自己应该有width那么宽,height那么高,最后就得用setMeasuredDimension(width, height)这个函数真正确定自己的高宽。然后onMeasure()的工作就完了。

    然后就是onDraw(Canvas canvas),这个就简单了,canvas就是父View给的一块画布,爱在上面画啥都行,比如写个字drawText(String text,float x, float y, Paint paint),

    text是要写的字,paint是写字的笔,值得注意的是x,y坐标是相对于你自己这一小块画布的左上角的。最左上就是0,0右下是width,height

    上代码

    /**
     * @author huangwei
     * @version SocialClient 1.2.0
     * @功能 图文混排TextView,请使用{@link #setMText(CharSequence)}
     * @2014年5月27日
     * @下午5:29:27
     */
    public class MTextView extends TextView
    {
        /**
         * 缓存测量过的数据
         */
        private static HashMap<String, SoftReference<MeasuredData>> measuredData = new HashMap<String, SoftReference<MeasuredData>>();
        private static int hashIndex = 0;
        /**
         * 存储当前文本内容,每个item为一行
         */
        ArrayList<LINE> contentList = new ArrayList<LINE>();
        private Context context;
        /**
         * 用于测量字符宽度
         */
        private TextPaint paint = new TextPaint();
        
    //    private float lineSpacingMult = 0.5f;
        private int textColor = Color.BLACK;
        //行距
        private float lineSpacing;
        private int lineSpacingDP = 5;
        /**
         * 最大宽度
         */
        private int maxWidth;
        /**
         * 只有一行时的宽度
         */
        private int oneLineWidth = -1;
        /**
         * 已绘的行中最宽的一行的宽度
         */
        private float lineWidthMax = -1;
        /**
         * 存储当前文本内容,每个item为一个字符或者一个SpanObject
         */
        private ArrayList<Object> obList = new ArrayList<Object>();
        /**
         * 是否使用默认{@link #onMeasure(int, int)}和{@link #onDraw(Canvas)}
         */
        private boolean useDefault = false;
        private CharSequence text = "";
    
        private int minHeight;
        /**
         * 用以获取屏幕高宽
         */
        private DisplayMetrics displayMetrics;
        /**
         * {@link android.text.style.BackgroundColorSpan}用
         */
        private Paint textBgColorPaint = new Paint();
        /**
         * {@link android.text.style.BackgroundColorSpan}用
         */
        private Rect textBgColorRect = new Rect();
    
        public MTextView(Context context)
        {
            super(context);
            this.context = context;
            paint.setAntiAlias(true);
            lineSpacing = dip2px(context, lineSpacingDP);
            minHeight = dip2px(context, 30);
    
            displayMetrics = new DisplayMetrics();
        }
        
        public MTextView(Context context,AttributeSet attrs)
        {
            super(context,attrs);
            this.context = context;
            paint.setAntiAlias(true);
            lineSpacing = dip2px(context, lineSpacingDP);
            minHeight = dip2px(context, 30);
    
            displayMetrics = new DisplayMetrics();
        }
    
        public static int px2sp(Context context, float pxValue)
        {
            final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
            return (int) (pxValue / fontScale + 0.5f);
        }
    
        /**
         * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
         */
        public static int dip2px(Context context, float dpValue)
        {
            final float scale = context.getResources().getDisplayMetrics().density;
            return (int) (dpValue * scale + 0.5f);
        }
    
        @Override
        public void setMaxWidth(int maxpixels)
        {
            super.setMaxWidth(maxpixels);
            maxWidth = maxpixels;
        }
    
        @Override
        public void setMinHeight(int minHeight)
        {
            super.setMinHeight(minHeight);
            this.minHeight = minHeight;
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
        {
            if (useDefault)
            {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                return;
            }
            int width = 0, height = 0;
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            switch (widthMode)
            {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
                width = widthSize;
                break;
            case MeasureSpec.UNSPECIFIED:
    
                ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
                width = displayMetrics.widthPixels;
                break;
            default:
                break;
            }
            if (maxWidth > 0)
                width = Math.min(width, maxWidth);
    
            paint.setTextSize(this.getTextSize());
            paint.setColor(textColor);
            int realHeight = measureContentHeight((int) width);
    
            //如果实际行宽少于预定的宽度,减少行宽以使其内容横向居中
            int leftPadding = getCompoundPaddingLeft();
            int rightPadding = getCompoundPaddingRight();
            width = Math.min(width, (int) lineWidthMax + leftPadding + rightPadding);
    
            if (oneLineWidth > -1)
            {
                width = oneLineWidth;
            }
            switch (heightMode)
            {
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
                height = realHeight;
                break;
            case MeasureSpec.UNSPECIFIED:
                height = realHeight;
                break;
            default:
                break;
            }
    
            height += getCompoundPaddingTop() + getCompoundPaddingBottom();
    
            height = Math.max(height, minHeight);
    
            setMeasuredDimension(width, height);
        }
    
        @Override
        protected void onDraw(Canvas canvas)
        {
            if (useDefault)
            {
                super.onDraw(canvas);
                return;
            }
            if (contentList.isEmpty())
                return;
            int width;
    
            Object ob;
    
            int leftPadding = getCompoundPaddingLeft();
            int topPadding = getCompoundPaddingTop();
    
            float height = 0 + topPadding + lineSpacing;
            //只有一行时
            if (oneLineWidth != -1)
            {
                height = getMeasuredHeight() / 2 - contentList.get(0).height / 2;
            }
    
            for (LINE aContentList : contentList)
            {
                //绘制一行
                float realDrawedWidth = leftPadding;
                for (int j = 0; j < aContentList.line.size(); j++)
                {
                    ob = aContentList.line.get(j);
                    width = aContentList.widthList.get(j);
    
                    if (ob instanceof String)
                    {
                        canvas.drawText((String) ob, realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
                        realDrawedWidth += width;
                    }
                    else if (ob instanceof SpanObject)
                    {
                        Object span = ((SpanObject) ob).span;
                        if(span instanceof ImageSpan)
                        {
                            ImageSpan is = (ImageSpan) span;
                            Drawable d = is.getDrawable();
        
                            int left = (int) (realDrawedWidth);
                            int top = (int) height;
                            int right = (int) (realDrawedWidth + width);
                            int bottom = (int) (height + aContentList.height);
                            d.setBounds(left, top, right, bottom);
                            d.draw(canvas);
                            realDrawedWidth += width;
                        }
                        else if(span instanceof BackgroundColorSpan)
                        {
                            
                            textBgColorPaint.setColor(((BackgroundColorSpan) span).getBackgroundColor());
                            textBgColorPaint.setStyle(Style.FILL);
                            textBgColorRect.left = (int) realDrawedWidth;
                            int textHeight = (int) getTextSize();
                            textBgColorRect.top = (int) (height + aContentList.height - textHeight - paint.getFontMetrics().descent);
                            textBgColorRect.right = textBgColorRect.left+width;
                            textBgColorRect.bottom = (int) (height + aContentList.height + lineSpacing  - paint.getFontMetrics().descent);
                            canvas.drawRect(textBgColorRect, textBgColorPaint);
                            canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
                            realDrawedWidth += width;
                        }
                        else//做字符串处理
                        {
                            canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
                            realDrawedWidth += width;
                        }
                    }
    
                }
                height += aContentList.height + lineSpacing;
            }
    
        }
    
        @Override
        public void setTextColor(int color)
        {
            super.setTextColor(color);
            textColor = color;
        }
    
        /**
         * 用于带ImageSpan的文本内容所占高度测量
         * @param width 预定的宽度
         * @return 所需的高度
         */
        private int measureContentHeight(int width)
        {
            int cachedHeight = getCachedData(text.toString(), width);
    
            if (cachedHeight > 0)
            {
                return cachedHeight;
            }
    
            // 已绘的宽度
            float obWidth = 0;
            float obHeight = 0;
    
            float textSize = this.getTextSize();
            FontMetrics fontMetrics = paint.getFontMetrics();
            //行高
            float lineHeight = fontMetrics.bottom - fontMetrics.top;
            //计算出的所需高度
            float height = lineSpacing;
    
            int leftPadding = getCompoundPaddingLeft();
            int rightPadding = getCompoundPaddingRight();
    
            float drawedWidth = 0;
            
            boolean splitFlag = false;//BackgroundColorSpan拆分用
    
            width = width - leftPadding - rightPadding;
    
            oneLineWidth = -1;
    
            contentList.clear();
    
            StringBuilder sb;
    
            LINE line = new LINE();
    
            for (int i = 0; i < obList.size(); i++)
            {
                Object ob = obList.get(i);
    
                if (ob instanceof String)
                {
    
                    obWidth = paint.measureText((String) ob);
                    obHeight = textSize;
                }
                else if (ob instanceof SpanObject)
                {
                    Object span = ((SpanObject) ob).span;
                    if(span instanceof ImageSpan)
                    {
                        Rect r = ((ImageSpan)span).getDrawable().getBounds();
                        obWidth = r.right - r.left;
                        obHeight = r.bottom - r.top;
                        if (obHeight > lineHeight)
                            lineHeight = obHeight;
                    }
                    else if(span instanceof BackgroundColorSpan)
                    {
                        String str = ((SpanObject) ob).source.toString();
                        obWidth = paint.measureText(str);
                        obHeight = textSize;
                        
                        //如果太长,拆分
                        int k= str.length()-1;
                        while(width - drawedWidth < obWidth)
                        {
                            obWidth = paint.measureText(str.substring(0,k--));
                        }
                        if(k < str.length()-1)
                        {
                            splitFlag = true;
                            SpanObject so1 = new SpanObject();
                            so1.start = ((SpanObject) ob).start;
                            so1.end = so1.start + k;
                            so1.source = str.substring(0,k+1);
                            so1.span = ((SpanObject) ob).span;
                            
                            SpanObject so2 = new SpanObject();
                            so2.start =  so1.end;
                            so2.end = ((SpanObject) ob).end;
                            so2.source = str.substring(k+1,str.length());
                            so2.span = ((SpanObject) ob).span;
                            
                            ob = so1;
                            obList.set(i,so2);
                            i--;
                        }
                    }//做字符串处理
                    else
                    {
                        String str = ((SpanObject) ob).source.toString();
                        obWidth = paint.measureText(str);
                        obHeight = textSize;
                    }
                }
    
                //这一行满了,存入contentList,新起一行
                if (width - drawedWidth < obWidth || splitFlag)
                {
                    splitFlag = false;
                    contentList.add(line);
    
                    if (drawedWidth > lineWidthMax)
                    {
                        lineWidthMax = drawedWidth;
                    }
                    drawedWidth = 0;
                    height += line.height + lineSpacing;
    
                    lineHeight = obHeight;
    
                    line = new LINE();
                }
    
                drawedWidth += obWidth;
    
                if (ob instanceof String && line.line.size() > 0 && (line.line.get(line.line.size() - 1) instanceof String))
                {
                    int size = line.line.size();
                    sb = new StringBuilder();
                    sb.append(line.line.get(size - 1));
                    sb.append(ob);
                    ob = sb.toString();
                    obWidth = obWidth + line.widthList.get(size - 1);
                    line.line.set(size - 1, ob);
                    line.widthList.set(size - 1, (int) obWidth);
                    line.height = (int) lineHeight;
    
                }
                else
                {
                    line.line.add(ob);
                    line.widthList.add((int) obWidth);
                    line.height = (int) lineHeight;
                }
    
            }
            
            if (drawedWidth > lineWidthMax)
            {
                lineWidthMax = drawedWidth;
            }
            
            if (line != null && line.line.size() > 0)
            {
                contentList.add(line);
                height += lineHeight + lineSpacing;
            }
            if (contentList.size() <= 1)
            {
                oneLineWidth = (int) drawedWidth + leftPadding + rightPadding;
                height = lineSpacing + lineHeight + lineSpacing;
            }
    
            cacheData(width, (int) height);
            return (int) height;
        }
    
        /**
         * 获取缓存的测量数据,避免多次重复测量
         * @param text
         * @param width
         * @return height
         */
        @SuppressWarnings("unchecked")
        private int getCachedData(String text, int width)
        {
            SoftReference<MeasuredData> cache = measuredData.get(text);
            if (cache == null)
                return -1;
            MeasuredData md = cache.get();
            if (md != null && md.textSize == this.getTextSize() && width == md.width)
            {
                lineWidthMax = md.lineWidthMax;
                contentList = (ArrayList<LINE>) md.contentList.clone();
                oneLineWidth = md.oneLineWidth;
    
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < contentList.size(); i++)
                {
                    LINE line = contentList.get(i);
                    sb.append(line.toString());
                }
                return md.measuredHeight;
            }
            else
                return -1;
        }
    
        /**
         * 缓存已测量的数据
         * @param width
         * @param height
         */
        @SuppressWarnings("unchecked")
        private void cacheData(int width, int height)
        {
            MeasuredData md = new MeasuredData();
            md.contentList = (ArrayList<LINE>) contentList.clone();
            md.textSize = this.getTextSize();
            md.lineWidthMax = lineWidthMax;
            md.oneLineWidth = oneLineWidth;
            md.measuredHeight = height;
            md.width = width;
            md.hashIndex = ++hashIndex;
    
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < contentList.size(); i++)
            {
                LINE line = contentList.get(i);
                sb.append(line.toString());
            }
    
            SoftReference<MeasuredData> cache = new SoftReference<MeasuredData>(md);
            measuredData.put(text.toString(), cache);
        }
    
        /**
         * 用本函数代替{@link #setText(CharSequence)}
         * @param cs
         */
        public void setMText(CharSequence cs)
        {
            text = cs;
    
            obList.clear();
    
            ArrayList<SpanObject> isList = new ArrayList<MTextView.SpanObject>();
            useDefault = false;
            if (cs instanceof SpannableString)
            {
                SpannableString ss = (SpannableString) cs;
                CharacterStyle[] spans = ss.getSpans(0, ss.length(), CharacterStyle.class);
                for (int i = 0; i < spans.length; i++)
                {
                    
                    int s = ss.getSpanStart(spans[i]);
                    int e = ss.getSpanEnd(spans[i]);
                    SpanObject iS = new SpanObject();
                    iS.span = spans[i];
                    iS.start = s;
                    iS.end = e;
                    iS.source = ss.subSequence(s, e);
                    isList.add(iS);
                }
            }
            
            //对span进行排序,以免不同种类的span位置错乱
            SpanObject[] spanArray = new SpanObject[isList.size()];
            isList.toArray(spanArray);
            Arrays.sort(spanArray,0,spanArray.length,new SpanObjectComparator());
            isList.clear();
            for(int i=0;i<spanArray.length;i++)
            {
                isList.add(spanArray[i]);
            }
            
            String str = cs.toString();
    
            for (int i = 0, j = 0; i < cs.length(); )
            {
                if (j < isList.size())
                {
                    SpanObject is = isList.get(j);
                    if (i < is.start)
                    {
                        Integer cp = str.codePointAt(i);
                        //支持增补字符
                        if (Character.isSupplementaryCodePoint(cp))
                        {
                            i += 2;
                        }
                        else
                        {
                            i++;
                        }
    
                        obList.add(new String(Character.toChars(cp)));
    
                    }
                    else if (i >= is.start)
                    {
                        obList.add(is);
                        j++;
                        i = is.end;
                    }
                }
                else
                {
                    Integer cp = str.codePointAt(i);
                    if (Character.isSupplementaryCodePoint(cp))
                    {
                        i += 2;
                    }
                    else
                    {
                        i++;
                    }
    
                    obList.add(new String(Character.toChars(cp)));
                }
            }
    
            requestLayout();
        }
    
        public void setUseDefault(boolean useDefault)
        {
            this.useDefault = useDefault;
            if (useDefault)
            {
                this.setText(text);
                this.setTextColor(textColor);
            }
        }
        /**
         * 设置行距
         * @param lineSpacingDP 行距,单位dp
         */
        public void setLineSpacingDP(int lineSpacingDP)
        {
            this.lineSpacingDP = lineSpacingDP;
            lineSpacing = dip2px(context, lineSpacingDP);
        }
        /**
         * 获取行距
         * @return 行距,单位dp
         */
        public int getLineSpacingDP()
        {
            return lineSpacingDP;
        }
        /**
         * @author huangwei
         * @version SocialClient 1.2.0
         * @功能: 存储Span对象及相关信息
         * @2014年5月27日
         * @下午5:21:37
         */
        class SpanObject
        {
            public Object span;
            public int start;
            public int end;
            public CharSequence source;
        }
        /**
         * @功能: 对SpanObject进行排序
         * @author huangwei
         * @2014年6月4日
         * @下午5:21:30
         * @version SocialClient 1.2.0
         */
        class SpanObjectComparator implements Comparator<SpanObject>
        {
            @Override
            public int compare(SpanObject lhs, SpanObject rhs)
            {
                
                return lhs.start - rhs.start;
            }
            
        }
        /**
         * @author huangwei
         * @version SocialClient 1.2.0
         * @功能: 存储测量好的一行数据
         * @2014年5月27日
         * @下午5:22:12
         */
        class LINE
        {
            public ArrayList<Object> line = new ArrayList<Object>();
            public ArrayList<Integer> widthList = new ArrayList<Integer>();
            public int height;
    
            @Override
            public String toString()
            {
                StringBuilder sb = new StringBuilder("height:" + height + "   ");
                for (int i = 0; i < line.size(); i++)
                {
                    sb.append(line.get(i) + ":" + widthList.get(i));
                }
                return sb.toString();
            }
    
        }
    
        /**
         * @author huangwei
         * @version SocialClient 1.2.0
         * @功能: 缓存的数据
         * @2014年5月27日
         * @下午5:22:25
         */
        class MeasuredData
        {
            public int measuredHeight;
            public float textSize;
            public int width;
            public float lineWidthMax;
            public int oneLineWidth;
            public int hashIndex;
            ArrayList<LINE> contentList;
    
        }

    为方便在ListView中使用(ListView反复上下滑动会多次重新onMeasure),加了缓存,相同的情况下可以不用重复在测量一次。

    对于SpannableString,只支持了ImageSpan,有其它需要者可自行扩展

    Demo:http://download.csdn.net/detail/yellowcath/7421147 或:https://github.com/yellowcath/MTextView.git (2014/6/4 更新 添加对BackGroundColorSpan的支持,修复一个会导致最后一行最后一个图形显示不全的bug)

    代码:这里

  • 相关阅读:
    django+xadmin在线教育平台(十一)
    pycharm界面美化,个人喜欢
    django+xadmin在线教育平台(十)
    django+xadmin在线教育平台(九)
    django+xadmin在线教育平台(八)
    django+xadmin在线教育平台(七)
    与其放在电脑里占内存,还不如拿出来帮助一群小白白之html篇之三
    与其放在电脑里占内存,还不如拿出来帮助一群小白白之html篇之二
    与其放在电脑里占内存,还不如拿出来帮助一群小白白之html篇之一
    STL略观——deque迭代器的一些关键行为
  • 原文地址:https://www.cnblogs.com/zhujiabin/p/4269217.html
Copyright © 2011-2022 走看看