Android考试答题控件&图文输入框混排

前言

看了某大神生动有趣的博客之后,萌生了把自己在项目中所遇到的一些技术点记录下来,一般都是网上很少教程的一些技术。这是我人生中的第一篇博客。写完之后看看能不能去杀个程序员祭天!!


面对疾风吧!

某天,老板叫我到办公室去唠嗑,他说小陈啊,最近小学生沉迷网络,我们是否可以做一款考试的软件来解救一下祖国花朵啊!
我陷入了沉默,皱了皱眉一副若有所思的摸样。老板见状,以我在思考,便没有打断我。随后我说:嗯 我回去想想!
其实不就是一个EditText加一个TextView的事情吗,哈哈哈.....机智如我。


理想状态

马不停蹄的就把这个屁屁给撸了出来。屁颠屁颠的跑过去跟他怼了一波,这么简单完全不能体现本公司的逼格。你回去看看猿题库是怎么做的。我就知道事情没有那么简单!


进入主题吧!

一开始是想用Html.formHtml()来实现的。但是这只能实现图文混排,不能增加输入框。
所以自己写了一个自定义View。

客官看得懂吗?

这个自定义View就是在onDraw()里面绘制文字,当碰到换行符或者绘制满一行时就换行,当碰到透明占位图时候绘制一个透明的图片,然后得到该占位图位置的坐标,然后设置EditText的位置就可以了。
Html.ImageGetter可以识别Html的标签,所以占位符的位置和占位符命名格式可以由我们自己来定义。以下是我自己定义的题目:

"一、填空题(共4小题)。 2.决定花儿的生长因素有(![](input_1_4)) (![](input_2_4)) (![](input_3_4))

这里的input是自己定义的名字,我这里的input代表着这是一个edittext输入框,数字" 1 "代表是第几个输入框,数字" 4 "代表该输入框里面的字符数是多少,用来定义输入框的长度。
当然你也可以增加题目所需要的图片:![](图片的地址)

代码走一波:

首先对题目处理一下,因为占位图的话需要两个符号来替换:

 String subject = "一、填空题(共4小题)。" +
                " 2.决定花儿的生长因素有(<img src=\"input_1_4\">) (<img src=\"input_2_4\">) (<img src=\"input_3_4\">)";

        //动态在图片标签前面加两个空格来作为图片插入时替换的无用字符
        //否则会替换掉原有的字符
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < subject.length(); i++) {
            char cha = subject.charAt(i);
            String chaStr = String.valueOf(cha);
            if (chaStr.equals("<")) {
                sb.append("  ");
            }

            sb.append(subject.charAt(i));
        }
        mSubJectSb = new SpannableString(sb.toString());

这里要注意的是img标签前面必须要加两个空格以备占位图替换,否则会出现题目不全的问题

接下来要处理一下img标签了:

 Html.fromHtml(sb.toString(), getImageGetterEdtText(getActivity(), mSubjectView, mAnswer.answer),
                new MTagHandler());

getImageGetterEdtText()方法:

 /**
     *  ImageGetter用处处理标签
     * @param context
     * @param mSubjectView
     * @param answer    该题目的答案
     * @return
     */
    public static Html.ImageGetter getImageGetterEdtText(final Context context, final TextView mtextView, final String answer) {
        DisplayMetrics mDisplayMetrics = new DisplayMetrics();
        ((Activity) context).getWindowManager()
                .getDefaultDisplay().getRealMetrics(mDisplayMetrics);
        final int screenWidth = mDisplayMetrics.widthPixels; // 屏幕宽(像素,如:480px)
        final int screenHeight = mDisplayMetrics.heightPixels; // 屏幕高

        /**
         * 返回图片的数据
         */
        Html.ImageGetter imgGetter = new Html.ImageGetter() {
            @Override
            public Drawable getDrawable(String source) {
                Drawable d = null;
                if (source.contains("input_") || source.contains("note_")) {

                    //这里是打开一张透明的图片
                    d = context.getResources().getDrawable(R.drawable.edttext_bg);
                    //得到该占位图的长度,比如之前的数字4
                    int size = Integer.valueOf(source.substring(source.lastIndexOf("_") + 1, source.length()));
                    //这里的note我是自己定义的另一个标签,可忽略
                    if (source.contains("note_"))
                        size = 1;

                    int width;
                    String[] answers = answer.split("\\|\\|");

                    float ratio = (float) 5 / size;
                    if (ratio < 1)
                        size = (int) (size * ratio);

                    if (size > 15)
                        size = 15;

                    if (size == 1) {
                        width = (int) (mtextView.getTextSize() * size + mtextView.getTextSize() / 2);
                    } else {
                        width = (int) (mtextView.getTextSize() * size);
                        if (source.contains("input_")) {
                            int sizeWidth = Integer.valueOf(source.substring(source.lastIndexOf("_") + 1, source.length()));
                            if (answers.length > 0) {
                                int mun = Integer.valueOf(source.substring(source.lastIndexOf("input_"), source.lastIndexOf("_")).replaceAll("input_", ""));
                                if (!checkChina(answers[mun - 1])) {
                                    width = (int) (mtextView.getTextSize() / 1.5 * sizeWidth);
                                }
                            } else {
                                if (!checkChina(answer)) {
                                    width = (int) (mtextView.getTextSize() / 1.5 * sizeWidth);
                                }
                            }
                        }
                    }
                    //                    int height = (int) (mtextView.getTextSize() * 1.5);
                    int height = (int) (mtextView.getTextSize());

                    if (screenWidth > screenHeight) {
                        float ratioWidth = screenWidth / 1280f;
                        width = Math.round(width * ratioWidth);
                    } else {
                        float ratioWidth = screenWidth / 800f;
                        width = Math.round(width * ratioWidth);
                    }

                    //设置该占位图的宽高
                    d.setBounds(0, 0, width, height);

                } else if (source.contains("sup_") || source.contains("sub_")) {
                } else {

                    d = Drawable.createFromPath(source);

                    BitmapFactory.Options options = new BitmapFactory.Options();
                    options.inJustDecodeBounds = true;
                    Bitmap bitmap = BitmapFactory.decodeFile(source, options); // 此时返回的bitmap为null

                    if (d != null) {
                        int width;
                        int height = options.outHeight;
                        if (options.outWidth > screenWidth) {
                            width = screenWidth - 100;
                        } else {
                            if (screenWidth > screenHeight) {
                                width = options.outWidth;
                                //                                float ratioWidth = screenWidth / 1280f;
                                float ratioWidth = screenWidth / 900f;
                                width = Math.round(width * ratioWidth);
                                float ratioHeight = (float) width / options.outWidth;
                                height = Math.round(height * ratioHeight);
                            } else {
                                width = options.outWidth;
                                //                                float ratioWidth = screenWidth / 800f;
                                float ratioWidth = screenWidth / 600f;
                                width = Math.round(width * ratioWidth);
                                float ratioHeight = (float) width / options.outWidth;
                                height = Math.round(height * ratioHeight);
                            }
                        }
                        if (width >= screenWidth) {
                            width = width - 50;
                        }
                        d.setBounds(0, 0, width, height);
                    }
                }

                return d;
            }
        };
        return imgGetter;
    }

MTagHandler()走一波:

    class MTagHandler implements Html.TagHandler {

        @Override
        public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
            // 处理标签<img>
            if (tag.toLowerCase(Locale.getDefault()).equals("img")) {

                // 获取长度
                int len = output.length();
                // 获取图片地址
                ImageSpan[] images = output.getSpans(len - 1, len, ImageSpan.class);
                String imgURL = images[0].getSource();

                //插入图片
                mSubJectSb.setSpan(images[0], len - 2, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

                if (imgURL.contains("input_")) {
                    EditText editText = new EditText(getActivity());
                    int size = Integer.valueOf(imgURL.substring(imgURL.lastIndexOf("_") + 1, imgURL.length()));
                    //将输入框保存到集合里
                    mEditTextList.add(editText);
                    editText.setTextColor(Color.BLACK);
                    //限定输入框能输入的字符数量
                    editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(size)});
                    editText.setSingleLine();
                    editText.setTextSize(25);
                    editText.setPadding(2, 2, 2, 2);
                    editText.setGravity(Gravity.CENTER);
                    editText.setHeight((int) (mSubjectView.getTextSize() * 1.5));
                    //设置虚拟键盘最下面的按钮是下一个
                    editText.setImeOptions(EditorInfo.IME_ACTION_NEXT);
                    //将输入框加入到布局容器
                    mSubjectRlMain.addView(editText);
                }

                // 使图片可点击并监听点击事件
                //                output.setSpan(new MyTagHandler.ClickableImage(mContext, imgURL), len - 1, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

数据都处理好了,接下来就是显示数据了:

        mSubjectView.setMText(mSubJectSb);
        mSubjectView.setEdtText(mEditTextList);
        //刷新控件,重走一遍onDraw
        mSubjectView.invalidate();
就是这么简单

是不是觉得很丑,确实是挺丑的,UI就这样啦,将就将就吧,你可以换一下EditText的背景啊,设置一下有焦点的状态无焦点的状态有内容的状态。
那么这个View里面到底做了什么呢?测量高度什么的就不详细说了,来说一下比较重要的onDraw()里面的代码。
因为canvas是逐步绘制嘛,首先先判断当前要绘制的内容格式是什么,如果内容的String的话,就直接canvas.drawText()
但是如果当前行有图片的话,当前行高就会比文字高度高出很多,所以绘制的文字是在最底部,想要文字居中的话(上图的效果)就要做一下判断:

if (ob instanceof String) {

                    //总高度大于文字高度,有图片在本行,文字居中
                    if (aContentList.height > (paint.getFontMetrics().descent * 2 + Math.abs(paint.getFontMetrics().ascent))) {
                        float textX =(height +
                                (aContentList.height + Math.abs(paint.getFontMetrics().ascent) 
                                        - paint.getFontMetrics().descent) / 2);
                        canvas.drawText((String) ob, realDrawedWidth, textX, paint);
                    } else {
                        canvas.drawText((String) ob, realDrawedWidth, y, paint);
                    }

                    realDrawedWidth += width;

                    //结尾时换行符/n 则换行
                    if (((String) ob).endsWith("\n") && j == aContentList.line.size() - 1) {
                        newParagraph = true;
                    }

                }

如果内容是图片的也需要判断,因为可能该行有两张图片,一张大一张小的,为了美观,小的要居中:

if (ob instanceof SpanObject) { //内容是标签
                    Object span = ((SpanObject) ob).span;
                    if (span instanceof ImageSpan) {
                        String source = ((ImageSpan) span).getSource();
                        int start = ((Spannable) text).getSpanStart(span);
                        int end = ((Spannable) text).getSpanEnd(span);
                        int picHeight = ((ImageSpan) span).getDrawable().getBounds().height();

                        if (aContentList.height > (picHeight * 1.3)) {
                            float textY = getTextHeight(height,aContentList)+picHeight/3;
                            float textTop = textY - aContentList.height;
                            float textBottom = textY + mFontMetrics.descent;
                            ((DynamicDrawableSpan) span).draw(canvas, text, start, end, (int) x, (int) textTop, (int) textY, (int) textBottom, paint);
                        } else {
                            ((DynamicDrawableSpan) span).draw(canvas, text, start, end, (int) x, (int) top, (int) y, (int) bottom, paint);

                            /**
                             * 图片的X轴=x , y轴=top
                             * 加上宽高等于范围
                             * 数据存起来,在onTouch里面判断处理,点击可以放大图片
                             * */
                        }

                        realDrawedWidth += width;
}

如果内容是图片,并且图片是占位图的话则需要设置对应的Edittext的位置:

 if (source.contains("input_")) { //如果是填空标签则edttext 定义位置
                            if (mEdtText != null && mEdtText.size() > 0) {
                                String mun = source.substring(source.lastIndexOf("input_"), source.lastIndexOf("_")).replaceAll("input_", "");
                                EditText editText = mEdtText.get(Integer.valueOf(mun) - 1 - currentIndex);
                                editText.setWidth(((ImageSpan) span).getDrawable().getBounds().width());
                                int imgWidth = ((ImageSpan) span).getDrawable().getBounds().width();
                                int imgHeight = ((ImageSpan) span).getDrawable().getBounds().height();
                                RelativeLayout.LayoutParams params = mEdtTextParams.get(Integer.valueOf(mun) - 1 - currentIndex);

                                if (aContentList.height > (imgHeight * 1.5)) {
                                    float textY = getTextHeight(height,aContentList);
                                    float textTop = textY - imgHeight;
                                    params.setMargins((int) (x), (int) textTop, 0, 0);

                                } else {
                                    if (aContentList == contentList.get(0)) {
                                        params.setMargins((int) (x), (int) top + imgHeight / 4,  0,  0);
                                    } else {
                                        params.setMargins((int) (x), (int) top,  0,  0);
                                    }
                                }

                                editText.setLayoutParams(params);
                            }
                        }

我只能说到这里了,剩下的就靠你们自己的造化了,下面是SubjectView的完整代码:

public class SubjectView 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 TextPaint upDownPaint = new TextPaint();
    /**
     * 用于测量span高度
     */
    private Paint.FontMetricsInt mSpanFmInt = new Paint.FontMetricsInt();
    /**
     * 临时使用,以免在onDraw中反复生产新对象
     */
    private FontMetrics mFontMetrics = new FontMetrics();

    //  private float lineSpacingMult = 0.5f;
    private int textColor = Color.BLACK;
    //行距
    private float lineSpacing;
    private int lineSpacingDP = 5;
    /**
     * 段间距,-1为默认
     */
    private int paragraphSpacing = -1;
    /**
     * 最大宽度
     */
    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;
    protected CharSequence text = "";

    private int minHeight;
    /**
     * 用以获取屏幕高宽
     */
    private DisplayMetrics displayMetrics;
    /**
     * {@link BackgroundColorSpan}用
     */
    private Paint textBgColorPaint = new Paint();
    /**
     * {@link BackgroundColorSpan}用
     */
    private Rect textBgColorRect = new Rect();
    private List<EditText> mEdtText;
    private int edtTextMun = 0;
    private List<RelativeLayout.LayoutParams> mEdtTextParams;

    private SpannableStringBuilder mStringBuilder = new SpannableStringBuilder("");
    private int mViewWidth;
    private List<ImageView> mImageViews;
    private List<RelativeLayout.LayoutParams> mImageViewParams;

    private int currentIndex = 0;
    private float mHeighRatio;

    public SubjectView(Context context) {
        super(context);
        init(context);
    }

    public SubjectView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SubjectView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }


    public void init(Context context) {
        this.context = context;
        paint.setAntiAlias(true);
        upDownPaint.setAntiAlias(true);
        lineSpacing = dip2px(context, lineSpacingDP);
        minHeight = dip2px(context, 30);

        displayMetrics = new DisplayMetrics();
        ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);

        mHeighRatio = 0f;
        if (displayMetrics.heightPixels > displayMetrics.widthPixels) {
            float ratio = (displayMetrics.heightPixels / 800f);
            if (ratio > 1.1) {
                mHeighRatio = 5 * ratio;
            }
        }
    }

    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:

                width = displayMetrics.widthPixels;
                break;
            default:
                break;
        }
        if (maxWidth > 0) {
            width = Math.min(width, maxWidth);
        }

        paint.setTextSize(this.getTextSize());
        paint.setColor(textColor);
        upDownPaint.setTextSize(this.getTextSize() / 2);
        upDownPaint.setColor(textColor);
        int realHeight = measureContentHeight(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));
        //        height = (int) (Math.max(height, minHeight) + contentList.get(0).height*1.2);

        mViewWidth = width;
        setMeasuredDimension(width, height);
    }

    private float getTextHeight(float height, LINE aContentList) {
        return (height + (aContentList.height + Math.abs(paint.getFontMetrics().ascent) - paint.getFontMetrics().descent) / 2);
    }

    @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;
            /** 是否换新段落*/
            boolean newParagraph = false;
            for (int j = 0; j < aContentList.line.size(); j++) {
                ob = aContentList.line.get(j);
                width = aContentList.widthList.get(j);
                paint.getFontMetrics(mFontMetrics);
                float x = realDrawedWidth;
                float y = height + aContentList.height - paint.getFontMetrics().descent;
                float top = y - aContentList.height;
                float bottom = y + mFontMetrics.descent;
                if (ob instanceof String) {

                    //总高度大于文字高度,有图片在本行,文字居中
                    if (aContentList.height > (paint.getFontMetrics().descent * 2 + Math.abs(paint.getFontMetrics().ascent))) {
                        float textX =(height +
                                (aContentList.height + Math.abs(paint.getFontMetrics().ascent) 
                                        - paint.getFontMetrics().descent) / 2);
                        canvas.drawText((String) ob, realDrawedWidth, textX, paint);
                    } else {
                        canvas.drawText((String) ob, realDrawedWidth, y, paint);
                    }

                    realDrawedWidth += width;

                    //结尾时换行符/n 则换行
                    if (((String) ob).endsWith("\n") && j == aContentList.line.size() - 1) {
                        newParagraph = true;
                    }

                } else if (ob instanceof SpanObject) { //内容是标签
                    Object span = ((SpanObject) ob).span;
                    if (span instanceof ImageSpan) {
                        String source = ((ImageSpan) span).getSource();
                        int start = ((Spannable) text).getSpanStart(span);
                        int end = ((Spannable) text).getSpanEnd(span);
                        int picHeight = ((ImageSpan) span).getDrawable().getBounds().height();

                        if (aContentList.height > (picHeight * 1.3)) {
                            float textY = getTextHeight(height,aContentList)+picHeight/3;
                            float textTop = textY - aContentList.height;
                            float textBottom = textY + mFontMetrics.descent;
                            ((DynamicDrawableSpan) span).draw(canvas, text, start, end, (int) x, (int) textTop, (int) textY, (int) textBottom, paint);
                        } else {
                            ((DynamicDrawableSpan) span).draw(canvas, text, start, end, (int) x, (int) top, (int) y, (int) bottom, paint);

                            /**
                             * 图片的X轴=x , y轴=top
                             * 加上宽高等于范围
                             * 数据存起来,在onTouch里面判断处理,点击可以放大图片
                             * */

                        }

                        realDrawedWidth += width;

                        if (source.contains("input_")) { //如果是填空标签则edttext 定义位置
                            if (mEdtText != null && mEdtText.size() > 0) {
                                String mun = source.substring(source.lastIndexOf("input_"), source.lastIndexOf("_")).replaceAll("input_", "");
                                EditText editText = mEdtText.get(Integer.valueOf(mun) - 1 - currentIndex);
                                editText.setWidth(((ImageSpan) span).getDrawable().getBounds().width());
                                int imgWidth = ((ImageSpan) span).getDrawable().getBounds().width();
                                int imgHeight = ((ImageSpan) span).getDrawable().getBounds().height();
                                RelativeLayout.LayoutParams params = mEdtTextParams.get(Integer.valueOf(mun) - 1 - currentIndex);

                                if (aContentList.height > (imgHeight * 1.5)) {
                                    float textY = getTextHeight(height,aContentList);
                                    float textTop = textY - imgHeight;
                                    params.setMargins((int) (x), (int) textTop, 0, 0);

                                } else {
                                    if (aContentList == contentList.get(0)) {
                                        params.setMargins((int) (x), (int) top + imgHeight / 4,  0,  0);
                                    } else {
                                        params.setMargins((int) (x), (int) top,  0,  0);
                                    }
                                }

                                editText.setLayoutParams(params);
                            }
                        } else if (source.contains("note_")) { //如果是图片填空标签则imageview 定义位置
                            if (mImageViews != null && mImageViews.size() > 0) {
                                String mun = source.substring(source.lastIndexOf("note_"), source.lastIndexOf("_")).replaceAll("note_", "");
                                ImageView imageView = mImageViews.get(Integer.valueOf(mun) - 1 - currentIndex);
                                imageView.setMaxWidth(((ImageSpan) span).getDrawable().getBounds().width());
                                int imgWidth = ((ImageSpan) span).getDrawable().getBounds().width();
                                int imgHeight = ((ImageSpan) span).getDrawable().getBounds().height();
                                RelativeLayout.LayoutParams params = mImageViewParams.get(Integer.valueOf(mun) - 1 - currentIndex);

                                if (aContentList.height > imgHeight * 1.5) {
                                    float textY = getTextHeight(height,aContentList);
                                    float textTop = textY - imgHeight;
                                    params.setMargins((int) (x), (int) textTop, 0, 0);
                                } else {
                                    if (aContentList == contentList.get(0)) {
                                        params.setMargins((int) (x), (int) top + imgHeight / 4, 0, 0);
                                    } else {
                                        params.setMargins((int) (x), (int) top, 0, 0);
                                    }
                                }

                                imageView.setLayoutParams(params);
                            }
                        }

                    } 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 - mFontMetrics.descent);
                        textBgColorRect.right = textBgColorRect.left + width;
                        textBgColorRect.bottom = (int) (height + aContentList.height + lineSpacing - mFontMetrics.descent);
                        canvas.drawRect(textBgColorRect, textBgColorPaint);
                        canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - mFontMetrics.descent, paint);
                        realDrawedWidth += width;
                    }  else//做字符串处理
                    {
                        canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - mFontMetrics.descent, paint);
                        realDrawedWidth += width;

                    }
                }

            }
            //如果要绘制段间距
            if (newParagraph) {
                height += aContentList.height + paragraphSpacing + getTextSize() / 2;
            } else {
                height += aContentList.height + lineSpacing + getTextSize() / 2;
            }
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        return true;
    }

    @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;
                if ("\n".equals(ob)) { //遇到"\n"则换行
                    obWidth = width - drawedWidth;
                }
            } else if (ob instanceof SpanObject) {
                Object span = ((SpanObject) ob).span;
                if (span instanceof DynamicDrawableSpan) {
                    int start = ((Spannable) text).getSpanStart(span);
                    int end = ((Spannable) text).getSpanEnd(span);
                    obWidth = ((DynamicDrawableSpan) span).getSize(getPaint(), text, start, end, mSpanFmInt);
                    obHeight = Math.abs(mSpanFmInt.top) + Math.abs(mSpanFmInt.bottom);
                    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;
                //判断是否有分段
                int objNum = line.line.size();
                if (paragraphSpacing > 0
                        && objNum > 0
                        && line.line.get(objNum - 1) instanceof String
                        && "\n".equals(line.line.get(objNum - 1))) {
                    height += line.height + paragraphSpacing + getTextSize() / 2;
                } else {
                    height += line.height + lineSpacing + getTextSize() / 2;
                }

                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<SpanObject>();
        useDefault = false;

        if (cs instanceof Spannable) {
            CharacterStyle[] spans = ((Spannable) cs).getSpans(0, cs.length(), CharacterStyle.class);
            for (int i = 0; i < spans.length; i++) {
                int s = ((Spannable) cs).getSpanStart(spans[i]);
                int e = ((Spannable) cs).getSpanEnd(spans[i]);
                SpanObject iS = new SpanObject();
                iS.span = spans[i];
                iS.start = s;
                iS.end = e;
                iS.source = cs.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);
    }

    public void setParagraphSpacingDP(int paragraphSpacingDP) {
        paragraphSpacing = dip2px(context, paragraphSpacingDP);
    }

    /**
     * 获取行距
     *
     * @return 行距,单位dp
     */
    public int getLineSpacingDP() {
        return lineSpacingDP;
    }

    public void setEdtText(List<EditText> edtText) {
        mEdtText = edtText;
        mEdtTextParams = new ArrayList<>();
        if (edtText.size() > 0) {
            for (EditText editText : edtText) {
                ViewGroup.LayoutParams layoutParams = new ViewGroup.MarginLayoutParams(edtText.get(0).getLayoutParams());
                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(layoutParams);
                mEdtTextParams.add(params);
            }
        }
    }

    public void setImageView(List<ImageView> imageView) {
        mImageViews = imageView;
        mImageViewParams = new ArrayList<>();
        if (imageView.size() > 0) {
            for (ImageView imageViewa : imageView) {
                ViewGroup.LayoutParams layoutParams = new ViewGroup.MarginLayoutParams(imageView.get(0).getLayoutParams());
                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(layoutParams);
                mImageViewParams.add(params);
            }
        }
    }

    /**
     * @author huangwei
     * @功能: 存储Span对象及相关信息
     * @2014年5月27日
     * @下午5:21:37
     */
    class SpanObject {
        public Object span;
        public int start;
        public int end;
        public CharSequence source;
    }

    /**
     * @author huangwei
     * @功能: 对SpanObject进行排序
     * @2017年5月19日16:59:06
     */
    class SpanObjectComparator implements Comparator<SpanObject> {
        @Override
        public int compare(SpanObject lhs, SpanObject rhs) {

            return lhs.start - rhs.start;
        }
    }

    /**
     * @author huangwei
     * @功能: 存储测量好的一行数据
     * @2017年5月19日16:59:23
     */
    class LINE {
        public ArrayList<Object> line = new ArrayList<Object>();
        public ArrayList<Integer> widthList = new ArrayList<Integer>();
        public float 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
     * @功能: 缓存的数据
     * @2017年5月19日16:59:31
     */
    class MeasuredData {
        public int measuredHeight;
        public float textSize;
        public int width;
        public float lineWidthMax;
        public int oneLineWidth;
        public int hashIndex;
        ArrayList<LINE> contentList;
    }
}

写完啦,第一次写博文可能会有点乱,有很多技术可能没有很完善,但是看到这个图文输入框混排的资料在网上很少,当初我也是找了很久才找到一点相关的,然后要自己研究出来,所以拿出来给大家参考参考。写的不好的话,大家多多包涵!
可怜的单休狗,快下班了哈哈哈~下次见。

周末愉快!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,048评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,658评论 22 664
  • 任何一个严肃认真的作家,为寻找一行富有创造性的文字,往往就像在沙子里面淘金一般不容易。如果说创造还有一点甜...
    D051飞鹰阅读 344评论 0 1
  • 很多人都觉得即使距离再遥远,自己还是很了解自己的老朋友,但是实际上这种直觉是错误的。因为陷入过去的回忆,而拼接出对...
    天水讼阅读 206评论 0 4
  • 世界任何人与机构,只要与他们相互接触,都存在着博弈。从经济学原理来讲,博弈的过程当中,每方都是倾向于自身的利益最大...
    koolmy阅读 1,352评论 0 1