关于TextView的一些特定操作,适用于社区app,论坛app,可以TextView显示Emoji 显示gif动图,@用户 ##话题 网址,以及一些自定义的文本

本文主要是对TextView 上的一些操作,适用于android 客户端开发的论坛app 中使用到的功能,比如显示居中显示emoji,居中显示gif 表情(可以扩展显示网络图片需要自己修改源码),给文字添加剧透(筛选任意一段文字添加一层有色的遮盖层),效果图

效果图

image

注:本项目使用到很多的TextView 的修饰类 诸如:SpannableStringBuilder,Spannable,ImageSpan,用法可以自行百度,很简答

所有的文件结构

image

1.文字中显示gif的做法是使用大佬的轮子 '

传送门 在这个基础上修改的

2.TextView 显示混合图片显示时,文字要居中,所以用到自定义 ImageSpan


public class VerticalImageSpan extends ImageSpan {
    public VerticalImageSpan(Bitmap b) {
        super(b);
    }

    public VerticalImageSpan(Drawable drawable) {
        super(drawable);
    }

    public VerticalImageSpan(Context context, int resourceId) {
        super(context, resourceId);
    }

    public VerticalImageSpan(Context context, Bitmap resourceId) {
        super(context, resourceId);
    }

    public VerticalImageSpan(Drawable d, String source) {
        super(d, source);
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end,
                       Paint.FontMetricsInt fontMetricsInt) {
        Drawable drawable = getDrawable();
        Rect rect = drawable.getBounds();
        if (fontMetricsInt != null) {
            Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
            int fontHeight = fmPaint.bottom - fmPaint.top;
            int drHeight = rect.bottom - rect.top;

            /**对于这里我表示,我不知道为啥是这样。不应该是fontHeight/2?但是只有fontHeight/4才能对齐
            难道是因为TextView的draw的时候top和bottom是大于实际的?具体请看下图
            所以fontHeight/4是去除偏差?*/
            int top = drHeight / 2 - fontHeight / 4;
            int bottom = drHeight / 2 + fontHeight / 4;

            fontMetricsInt.ascent = -bottom;
            fontMetricsInt.top = -bottom;
            fontMetricsInt.bottom = top;
            fontMetricsInt.descent = top;
        }
        return rect.right;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end,
                     float x, int top, int y, int bottom, Paint paint) {
       

        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
        Drawable drawable = getDrawable();
        int transY = (y + fm.descent + y + fm.ascent) / 2
                - drawable.getBounds().bottom / 2;
        canvas.save();
        canvas.translate(x, transY);
        drawable.draw(canvas);
        canvas.restore();
    }
}

3.SelectionBean 这个类的主要作用是用来记录需要一段字符串中存在些什么,便于文字的点击效果,剧透的点击效果等的处理


public class SelectionBean {

    private String id;
    private String name;
    private int start;
    private int end;
    private int type;//1 自定义的需要点击的文本    2 @用户  3标签   4 网址 跳webView    5其它类型 6 其它类型    这是收录的类型分类

    public SelectionBean(String id, String name, int start, int end, int type) {
        this.id = id;
        this.name = name;
        this.start = start;
        this.end = end;
        this.type = type;
    }

    public SelectionBean(String name, int start, int end, int type) {
        this.name = name;
        this.start = start;
        this.end = end;
        this.type = type;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStart() {
        return start;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public int getEnd() {
        return end;
    }

    public void setEnd(int end) {
        this.end = end;
    }}

4.FaceData 这个类主要是用来记录Emoji gif表情的名字 很重要不能丢失,
5.AppTools 一个简答的工具类,

class AppTools {
    companion object {
        fun dp2px(context: Context, value: Int): Int {
            return (context.resources.displayMetrics.density * value).toInt()
        }
    }
}

6.GlideImageGetter 这个类主要做用是展示Drawable,gif 和 静态emoji,这个地方,我们要注意这是一个无限循环的去绘制gif,所以要监听View 是否在桌面上显示,添加OnAttachStateChangeListener, 要控制Animatable 的开始和停止
这个地方不处理非常的消耗资源。

public class GlideImageGetter implements Html.ImageGetter, Drawable.Callback {

    private final Context mContext;

    private final TextView mTextView;

    private final Set mTargets;

    public static GlideImageGetter get(View view) {
        return (GlideImageGetter) view.getTag(R.id.drawable_callback_tag);
    }

    public void clear() {
        GlideImageGetter prev = get(mTextView);
        if (prev == null) return;

        for (ImageGetterViewTarget target : prev.mTargets) {
            Glide.clear(target);
        }
    }

    public GlideImageGetter(Context context, TextView textView) {
        this.mContext = context;
        this.mTextView = textView;

        //        clear(); 屏蔽掉这句在TextView中可以加载多张图片
        mTargets = new HashSet<>();
        mTextView.setTag(R.id.drawable_callback_tag, this);
    }

    @Override
    public Drawable getDrawable(String url) {
        final UrlDrawable_Glide urlDrawable = new UrlDrawable_Glide();
        Glide.with(mContext)
                .load(url)
                .override(1, 1)
                .diskCacheStrategy(DiskCacheStrategy.SOURCE)
                .into(new ImageGetterViewTarget(mTextView, urlDrawable));
        return urlDrawable;
    }

    @Override
    public void invalidateDrawable(Drawable who) {
        mTextView.invalidate();
    }

    @Override
    public void scheduleDrawable(Drawable who, Runnable what, long when) {

    }

    @Override
    public void unscheduleDrawable(Drawable who, Runnable what) {

    }

    private class ImageGetterViewTarget extends ViewTarget {

        private final UrlDrawable_Glide mDrawable;

        private ImageGetterViewTarget(TextView view, UrlDrawable_Glide drawable) {
            super(view);
            mTargets.add(this);
            this.mDrawable = drawable;
        }

        @Override
        public void onResourceReady(final GlideDrawable resource, GlideAnimation glideAnimation) {
            Rect rect;
            if (resource.getIntrinsicWidth() > 100) {
                float width;
                float height;
                System.out.println("Image width is " + resource.getIntrinsicWidth());
                System.out.println("View width is " + view.getWidth());
                if (resource.getIntrinsicWidth() >= getView().getWidth()) {
                    float downScale = (float) resource.getIntrinsicWidth() / getView().getWidth();
                    width = (float) resource.getIntrinsicWidth() / (float) downScale;
                    height = (float) resource.getIntrinsicHeight() / (float) downScale;
                } else {
                    float multiplier = (float) getView().getWidth() / resource.getIntrinsicWidth();
                    width = (float) resource.getIntrinsicWidth() * (float) multiplier;
                    height = (float) resource.getIntrinsicHeight() * (float) multiplier;
                }
                System.out.println("New Image width is " + width);
                rect = new Rect(8, 0, AppTools.Companion.dp2px(mContext, 15), AppTools.Companion.dp2px(mContext, 15));
            } else {
                rect = new Rect(8, 0, AppTools.Companion.dp2px(mContext, 15) + 8, AppTools.Companion.dp2px(mContext, 15));
            }
            resource.setBounds(rect);
            mDrawable.setBounds(rect);
            mDrawable.setDrawable(resource);
            if (resource.isAnimated()) {
                mDrawable.setCallback(get(getView()));
                resource.setLoopCount(GlideDrawable.LOOP_FOREVER);
                resource.start();
            }
// 这个非常重要
            getView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    /**
                     * 添加视图
                     */
                    if (resource != null)
                        resource.start();
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (resource != null)
                        if (resource.isRunning())
                            resource.stop();
                }
            });
            getView().setText(getView().getText());
            getView().invalidate();
        }

        private Request request;

        @Override
        public Request getRequest() {
            return request;
        }

        @Override
        public void setRequest(Request request) {
            this.request = request;
        }
    }
}
  1. AppConfig 配置文件,主要是一些正则表达式,表达式用户可以根据自己的情况修改,
public class AppConfig {
    public static final String AT = "@[\\w\\p{InCJKUnifiedIdeographs}-]{1,26}";// @人
    private static final String TOPIC = "#[\\p{Print}\\p{InCJKUnifiedIdeographs}&&[^#]]+#";// ##话题
    public static final String SPOLIER = "\\[剧透:[\\s\\S]*?]";
    public static final String EMOJI = ":[0-9a-zA-Z_]+:";//
//这个可以用系统自带的表达式
    public static final String URLPATH = "((http|https)://)(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\\&%_\\./-~-]*)?";
    public static final String ALL = "(" + AT + ")" + "|" + "(" + TOPIC + ")" + "|" + "(" + URLPATH + ")";
    private static Pattern mPattern = Pattern.compile(EMOJI);

    public static String getImageHtml(String text) {
        String result = text;
        try {
            Matcher matcher = mPattern.matcher(text);
            while (matcher.find()) {
                String group = matcher.group();
                if (group != null) {
                    String faceId = null;
                    if ((faceId = FaceData.gifFaceInfo.get(group)) != null) {
                        result = result.replaceFirst(group, "");
                    } /*else if ((faceId = FaceData.staticFaceInfo.get(group)) != null) {
                    result = result.replaceFirst(group, *//*""*//*faceId);
                }*/
                }
            }
            return result.replace("\n", "
"); } catch (Exception e) { return result; } } }

8.SpolierTextView 主要是绘制剧透层(灰色遮盖层),和绘制选中文字的前景,点击的背景,数字的点击事件等
这里我要学习一个类 import android.text.Layout 下的Layout,这个类记录了TextView 的所有位置信息,这些方法可以去了解一波


image.png
public class SpolierTextView extends AppCompatTextView {

    private List arrays = new ArrayList<>();
    private List selects = new ArrayList<>();
    private Paint mPaint;
    private Context mContext;
    private boolean isSpoiler = false;
    private boolean isAttchWindows = false;

    public SpolierTextView(Context context) {
        this(context, null, 0);
    }

    public SpolierTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SpolierTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initView();
    }

    private void initView() {
        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#6d6d6d"));
        mPaint.setStyle(Paint.Style.FILL);
        //        mPaint.setStrokeWidth(10);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /**
         * 绘制一层阴影
         */
        if (isAttchWindows)
            getMeasureCoondiration(canvas);
        /**
         * 获取第一行换行的角标
         */
    }

    /**
     * 获取字符的 rect
     * 判断当前文字在第几行
     * 同一行 直接绘制黑色块
     * 如果是已经换行   判断换行  且获取换行的矩形
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void getMeasureCoondiration(Canvas canvas) {

        Layout layout = getLayout();
        if (arrays.size() > 0) {
            for (int a = 0; a < arrays.size(); a++) {
                if (!isAttchWindows)
                    break;
                /**
                 * 获取一行的字数
                 */
                Rect bound = new Rect();
                /**
                 *当前文本所在行数
                 */
                int startLine = layout.getLineForOffset(arrays.get(a).x);
                /**
                 * 文本结束的行数
                 */

                int endLine = layout.getLineForOffset(arrays.get(a).y);
                int lineCount = layout.getLineCount();
                float lineSpacingExtra = getLineSpacingExtra();
                float lineSpacingMultiplier = getLineSpacingMultiplier();
                layout.getLineBounds(startLine, bound);
                //                int yAxisTop = bound.top;//字符顶部y坐标
                int yAxisBottom = bound.bottom;//字符底部y坐标
                int lineHeight = (int) ((getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) * 1f / lineCount + 0.5f);//计算单行的高度
                int yAxisTop = startLine * lineHeight;//字符顶部y坐标
                int xAxisLeft = (int) layout.getPrimaryHorizontal(arrays.get(a).x);//字符左边x坐标
                int xAxisRight = (int) layout.getSecondaryHorizontal(arrays.get(a).y);//偏移量
                if (false) {
                    continue;
                }
                if (startLine == endLine) {//只有一行的时候
                    canvas.drawRect(new RectF(xAxisLeft, lineHeight * startLine + 3, (int) layout.getSecondaryHorizontal(arrays.get(a).y), yAxisTop + lineHeight - 3), mPaint);
                } else {//多行
                    /**
                     * 换行绘制 需要绘制两行 黑色区域
                     */
                    canvas.drawRect(new RectF(xAxisLeft, yAxisTop + 3, bound.right, yAxisTop + lineHeight - 3), mPaint);
                    /**
                     * 循环绘制存在剧透的行数
                     */
                    if (endLine - startLine > 1) {
                        for (int i = startLine + 1; i < endLine; i++) {
                            if (!isAttchWindows)
                                return;
                            canvas.drawRect(new RectF(0, lineHeight * i + 3, bound.right, lineHeight * (i + 1) - 3), mPaint);
                        }
                    }
                    canvas.drawRect(new RectF(0, lineHeight * endLine + 3, xAxisRight, lineHeight * (endLine + 1) - 3), mPaint);
                }
            }
        }
    }

    public void setData(String text, String... tag) {
        arrays.clear();
        selects.clear();
        SpannableStringBuilder htmlStr = (SpannableStringBuilder) Html.fromHtml(text.toString());
        Pattern pattern = Pattern.compile(SPOLIER);
        Matcher matcher = pattern.matcher(htmlStr);
        while (matcher.find()) {//替换需要更改的文本
            final String at = matcher.group();
            if (at != null) {
                int start = matcher.start();
                int end = start + at.length();
                int orignalX = start - 5 * arrays.size();
                int orignalY = end - 5 * (arrays.size() + 1);
                arrays.add(new Point(orignalX, orignalY));
                htmlStr.delete(orignalX, orignalX + "[剧透:".length());
                htmlStr.delete(orignalY, orignalY + 1);
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            //            setTextIsSelectable(true);
        }
        setText(htmlStr);
        setMovementMethod(LinkMovementMethod.getInstance());
        final SpannableString tmep = (SpannableString) getText();
        if (tmep instanceof Spannable) {
            final int end = tmep.length();
            final Spannable sp = (Spannable) getText();
            ImageSpan[] imgs = tmep.getSpans(0, end, ImageSpan.class);
            for (ImageSpan url : imgs) {
                VerticalImageSpan span = new VerticalImageSpan(getUrlDrawable(url.getSource(), this), url.getSource());
                tmep.setSpan(span, tmep.getSpanStart(url), tmep.getSpanEnd(url), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }

        pattern = Pattern.compile(ALL);
        matcher.reset();
        matcher = pattern.matcher(tmep);
        while (matcher.find()) {
            String at = matcher.group(1);
            String topic = matcher.group(2);
            String urlPath = matcher.group(3);
            if (at != null) {
                int start = matcher.start(1);
                int endSpoiler = start + at.length();
                selects.add(new SelectionBean(at, start, endSpoiler, 2));
                ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#2d7dd2"));
                tmep.setSpan(colorSpan, start, endSpoiler, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            }
            if (topic != null) {
                int start = matcher.start(2);
                int endSpoiler = start + topic.length();
                selects.add(new SelectionBean(topic, topic, start, endSpoiler, 3));
                ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#2d7dd2"));
                tmep.setSpan(colorSpan, start, endSpoiler, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            }
            if (urlPath != null) {
                int start = matcher.start(3);
                int endUrl = start + urlPath.length();
                selects.add(new SelectionBean(urlPath, start, endUrl, 4));
                ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#2d7dd2"));
                tmep.setSpan(colorSpan, start, endUrl, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            }
        }
        pattern = Pattern.compile(EMOJI);
        matcher.reset();
        matcher = pattern.matcher(tmep);
        while (matcher.find()) {
            String emoji = matcher.group();
            if (emoji != null) {
                int start = matcher.start();
                int emojiEnd = start + emoji.length();
                String emojiPath = null;
                if ((emojiPath = FaceData.staticFaceInfo.get(emoji)) != null) {
                    try {
                        InputStream open = mContext.getAssets().open(emojiPath);
                        BitmapDrawable drawable = new BitmapDrawable(open);
                        drawable.setBounds(0, 0, AppTools.Companion.dp2px(mContext, 16), AppTools.Companion.dp2px(mContext, 16));
                        VerticalImageSpan span = new VerticalImageSpan(drawable);
                        tmep.setSpan(span, start, emojiEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        if (tag != null && tag.length > 0) {
            for (String str : tag)
                if (!TextUtils.isEmpty(str)) {
                    int start = tmep.toString().indexOf(str);
                    int end = tmep.toString().indexOf(str) + str.length();
                    ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.parseColor("#2d7dd2"));
                    try {
                        tmep.setSpan(colorSpan, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    selects.add(new SelectionBean("", start, end, 1));
                }
        }

        setText(tmep);
        final BackgroundColorSpan span = new BackgroundColorSpan(Color.parseColor("#31000000"));
        final int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
        setOnTouchListener(new OnTouchListener() {

            int downX, downY;
            int id;
            SelectionBean downSection = null;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = MotionEventCompat.getActionMasked(event);
                Layout layout = getLayout();
                if (layout == null) {
                    return false;
                }
                int line = 0;
                int index = 0;

                switch (action) {
                    case MotionEvent.ACTION_DOWN://TODO 最后一行点击问题 网址链接
                        int actionIndex = event.getActionIndex();
                        id = event.getPointerId(actionIndex);
                        downX = (int) event.getX(actionIndex);
                        downY = (int) event.getY(actionIndex);
                        line = layout.getLineForVertical(getScrollY() + (int) event.getY());
                        index = layout.getOffsetForHorizontal(line, (int) event.getX());
                        int lastRight = (int) layout.getLineRight(line);
                        if (lastRight < event.getX()) {  //文字最后为话题时,如果点击在最后一行话题之后,也会造成话题被选中效果
                            return false;
                        }
                        Point clickPoint = null;
                        for (Point point : arrays) {//判断是否存在点击剧透 取消剧透
                            if (index >= point.x && index <= point.y) {
                                clickPoint = point;
                            }
                        }
                        if (clickPoint != null) {
                            arrays.remove(clickPoint);
                            invalidate();
                        }
                        for (SelectionBean section : selects) {
                            if (index >= section.getStart() && index <= section.getEnd()) {
                                tmep.setSpan(span, section.getStart(), section.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                                downSection = section;
                                setText(tmep);
                                getParent().requestDisallowInterceptTouchEvent(true);//不允许父view拦截
                                return true;
                            }
                        }

                        return false;
                    case MotionEvent.ACTION_MOVE:
                        int indexMove = event.findPointerIndex(id);
                        /**
                         * 会出现的异常 pointerIndex out of range
                         */
                        int currentX = 0;
                        int currentY = 0;
                        try {
                            currentX = (int) event.getX(indexMove);
                            currentY = (int) event.getY(indexMove);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        if (Math.abs(currentX - downX) < slop && Math.abs(currentY - downY) < slop) {
                            if (downSection == null) {
                                getParent().requestDisallowInterceptTouchEvent(false);//允许父view拦截
                                return false;
                            }
                            break;
                        }
                        downSection = null;
                        getParent().requestDisallowInterceptTouchEvent(false);//允许父view拦截
                    case MotionEvent.ACTION_CANCEL:
                    case MotionEvent.ACTION_UP:
                        int indexUp = event.findPointerIndex(id);
                        tmep.removeSpan(span);
                        setText(tmep);
                        int upX = (int) event.getX(indexUp);
                        int upY = (int) event.getY(indexUp);
                        if (Math.abs(upX - downX) < slop && Math.abs(upY - downY) < slop) {
                            //TODO startActivity or whatever
                            if (downSection != null) {
                                if (downSection.getType() == 3) {//跳转搜索

                                } else if (downSection.getType() == 4) {

                                } else {
                                    if (mTextTouchListener != null) {
                                        mTextTouchListener.touch(downSection);
                                    }
                                }
                                downSection = null;
                            } else {
                                return false;
                            }
                        } else {
                            downSection = null;
                            return false;
                        }
                        break;
                }
                return true;
            }
        });
    }

    public interface OnTextTouchListener {
        void touch(SelectionBean bean);
    }

    private OnTextTouchListener mTextTouchListener;

    public void setOnTextTouchListener(OnTextTouchListener listener) {
        mTextTouchListener = listener;
    }

    public static Drawable getUrlDrawable(String source, TextView mTextView) {
        GlideImageGetter imageGetter = new GlideImageGetter(mTextView.getContext(), mTextView);
        return imageGetter.getDrawable(source);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        isAttchWindows = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isAttchWindows = false;
        //        arrays.clear();
        //        selects.clear();
    }
}

好了所有的要点都是这里了,可以直接复制进项目,调用的话,就这样,

String text = "[剧透:这是一段剧透文字] 动态表情:teasing::teasing::teasing:  emoji   :anguished::apple::art:  #我是标签可以点击的# @王昭君  https://www.baidu.com";
        SpolierTextView spoiler_Text =  findViewById(R.id.tv_Spoiler);
        spoiler_Text.setData(AppConfig.getImageHtml(text));

你可能感兴趣的:(关于TextView的一些特定操作,适用于社区app,论坛app,可以TextView显示Emoji 显示gif动图,@用户 ##话题 网址,以及一些自定义的文本)