TextView自定义轻松实现下划线、点击弹框

综合技术 2017-11-15

最近公司有意需求,就是类似于电子书,选择一段文字然后做笔记,需要给做过的文字加下划线,下划线最后加一图标按钮,点击弹框显示笔记内容。

立马会想到使用TextView的fromHtml方法,给添加笔记的文本手动加标签,或者使用SpanString类的相关方法设置标签。

但是!

经过反复测试,无论使用何种下划线标签或者SpanString设置下划线,画出的下划线颜色始终和文本内容颜色一样,还不能随便定义颜色。更何况:我们需要在下划线最后加图标,并且能够点击。看来这种方法不可行...

于是,便开始了我的自定义之路~~~~

先看效果图:

这是纯文本的TextView

这里写图片描述

这是富文本的TextView

这里写图片描述

分析

这里写图片描述

要实现以上需求,应该从这几个方面入手:

文本展示,普通文本调用TextView的setText方法既可,如果是富文本,就使用TextView的fromHtml方法,至于图片如何展示,我在上一篇文章 用TextView实现富文本展示,点击断句和语音播报 介绍过了,有兴趣的可以跳转阅读,核心是拦截到图片url然后自己实现加载图片。

给TextView设置要划线的起始位置和结束位置,需要计算出在哪些行进行绘制,每行又是从哪里开始,到哪里结束,注意第一行和最后一行。

然后就是在onDraw方法中对计算出的行进行逐行绘制,在最后一行的结束位置绘制笔记图标(小圆圈)。

在TextView的onTouchEvent判断按下位置是否是笔记图标(小圆圈)的附近,是的话则弹框(PopupWindow)显示。

文本显示

这里就不再重复累赘了,文本展示很简单:

调用setText或fromHtml方法既可。

颜色等属性设置

private Rect mRect;
private Paint mPaint;
private int mColor = 0xFFFFA200;
private float density;
private float mStrokeWidth;

// 笔记白点
private Paint mPointPaint;

// 开始各结束位置索引,startIndex必须大于等于endIndex
private int startIndex = 0;
private int endIndex = 0;

// 下划线的位置(每次更新)
private float x_start, x_stop, x_diff;
private int baseline;
// 小圆圈的位置
private float notePointX, notePointY;

private int scrollY = 0;

我们需要定义画笔、画笔颜色、线条粗细;开始位置的结束位置的索引。

还有就是下划线的位置,因为我们是 按行 来画,每画完一行就会重新计算,尤其是横向的结束位置,所以我将x的结束位置定义出来,每次都更新。

最后要将计算出的小图标的x和y值保留,在onTouchEvent中会用到。

并初始化:

//获取屏幕密度
density = getResources().getDisplayMetrics().density;

mStrokeWidth = density;

mRect = new Rect();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setColor(mColor);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(mStrokeWidth);

mPointPaint = new Paint();
mPointPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPointPaint.setColor(Color.WHITE);
mPointPaint.setAntiAlias(true);
mPointPaint.setStrokeWidth(2.2f);

计算划线位置

class TextIndex {
    int line;
    int start;
    int end;

    public TextIndex(int line, int start, int end) {
        this.line = line;
        this.start = start;
        this.end = end;
    }
}

我们先定义一个实体类,这个类中存放每行的索引,和对应每行上的一个开始位置索引,结束位置索引。

// 存放所有行,和对应的每行开始位置和结束位置
private List indexs = new ArrayList();
// 存放需要绘制的行,和每行对应的开始和结束的位置
private List drawIndexs = new ArrayList();

定义两个集合,分别存放所有行的信息和需要绘制的行的信息。

接下来开始计算:

for (int i = 0; i = indexs.get(i).start && startIndex = indexs.get(i).start && endIndex = indexs.get(i).start && endIndex <= indexs.get(i).end)="" {="" drawindexs.add(new="" textindex(i,="" indexs.get(i).start,="" endindex));="" hasstart="false;" break;="" 否则此行全画="" }="" else="" if="" (hasstart)="" indexs.get(i).end));="" } 
 

思路是这样的:

  1. 循环所有行;
  2. 如果要绘制的开始位置在这行中,并且结束位置也在这行中,直接向要绘制的集合中添加一个对象,终止循环;
  3. 如果开始位置在这行中,但结束位置不在这行中,则添加一个结束位置是本行结束位置的对象到要绘制的集中中,继续下次循环;
  4. 如果结束位置在此行,则添加开始位置为本行开始位置,结束位置为自己结束位置的对象到集合中;
  5. 否则,将整行填入集合。

绘制下划线

for (int i = 0; i < drawIndexs.size(); i++) {

    // getLineBounds得到这一行的外包矩形,
    // 这个字符的顶部Y坐标就是rect的top 底部Y坐标就是rect的bottom
    baseline = getLineBounds(drawIndexs.get(i).line, mRect);

    //要得到这个字符的左边X坐标 用layout.getPrimaryHorizontal
    //得到字符的右边X坐标用layout.getSecondaryHorizontal
    x_start = layout.getPrimaryHorizontal(drawIndexs.get(i).start);
    x_diff = layout.getPrimaryHorizontal(drawIndexs.get(i).start + 1) - x_start;
    x_stop = layout.getPrimaryHorizontal(drawIndexs.get(i).end - 1) + x_diff;
    canvas.drawLine(x_start, baseline + mStrokeWidth + 8, x_stop, baseline + mStrokeWidth + 8, mPaint);
}

核心使用的是canvas的drwaLine方法进行绘制。

循环所有要绘制的集合,得到这一行的外包矩形,根据当前行的开始和结束位置,算出横向x的开始和结束位置;baseline是字符底部y的值,这样就可以绘制划线了!

绘制笔记图标

/**
 * 在最后位置绘制椭圆和三个白点
 * 注意这里的所有值都不能给死,否则无法适配
 */
if (i == drawIndexs.size() - 1) {
    canvas.drawCircle(x_stop + mStrokeWidth * 4, baseline + mStrokeWidth + 8, mStrokeWidth * 4, mPaint);
    notePointX = x_stop + mStrokeWidth * 4;
    notePointY = baseline + mStrokeWidth + 8;
    Log.e(TAG, "onDraw: x=" + (x_stop + mStrokeWidth * 4) + "y=" + (baseline + mStrokeWidth + 8));
    float[] pts = {x_stop + mStrokeWidth * 2, baseline + mStrokeWidth + 8, x_stop + mStrokeWidth * 4, baseline + mStrokeWidth + 8, x_stop + mStrokeWidth * 6, baseline + mStrokeWidth + 8};
    canvas.drawPoints(pts, mPointPaint);
}

如果是最后一行的,在本行的结束位置开始绘制笔记图标。

使用canvas.drawCircle绘制圆圈,而圆的圆形坐标可以下划线最后的位置进行绘制。

再用另一条画笔绘制三个白点,这个白点可以使用canvas.drawPoints绘制,传入一个float类型数组,下标是奇数,表示点的x值,下表为偶数,表示点的y值,也就是说float数组的个数必须是偶数个,或者说是点数的两倍。

图标点击

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent: " + event.getX() + "   " + event.getY());
                Log.e(TAG, "onTouchEvent: " + event.getX() + "   " + getScrollY());

                if (Math.abs(event.getX() - notePointX) <= 30="" &&="" math.abs(event.gety()="" -="" notepointy)="" <="30)" {="" jspopupwindow="" popwindow="new" jspopupwindow.builder()="" .setcontentviewid(r.layout.dialog_popupwindow)="" 设置布局="" .setcontext(getcontext())="" 设置上下文="" .setoutsidecancle(true)="" 点击外部消失="" .setheight(linearlayout.layoutparams.wrap_content)="" 设置高度="" .setwidth(linearlayout.layoutparams.wrap_content)="" 设置宽度="" .setanimation(r.style.anim_pop)="" 设置动画="" .build()="" 构建="" .showatlocation(this,="" gravity.top="" |="" gravity.left,="" (int)="" notepointx,="" notepointy="" scrolly);="" textview="" tv_pop="(TextView)" popwindow.getitemview(r.id.tv_pop);="" tv_pop.settext("我爱北京天安门,天安门上太阳升");="" }="" break;="" case="" motionevent.action_move:="" motionevent.action_up:="" return="" true;="" } 
 

在上一步绘制小图标时,就将图标的x和y值保存,在onTouchEvent中,判断按下的位置是否在小图标位置的“附近”,是的话就弹框显示笔记内容。

这里的弹框用的是我之前封装的JsPopupWindow,有兴趣的话可以点击阅读 github.com/shuaijia/Js…

这里需要注意,如果TextView外层被ScrollView包裹,在弹框是就需要纵轴方向上减去ScrollView的偏移量。也就是TextView需要知道ScrollView的纵向偏移量,这里我设置了方法,将ScrollView的偏移量传入。

scroll_rich.setOnScrollChangeListener(new View.OnScrollChangeListener() {
    @Override
    public void onScrollChange(View view, int i, int i1, int i2, int i3) {
        tv_rich_note.setMScrollY(i1);
    }
});

这样就实现了我们如上图展示的,给TextView绘制下划线和图标点击,弹框的效果。

想获取更多精彩,请关注我的微信公众号—— 安卓干货营

这里写图片描述
稀土掘金

责编内容by:稀土掘金 (源链)。感谢您的支持!

您可能感兴趣的

NullPointerException of the fragment function when... I'm trying to create an app that lists cities in one fragment and when clicked, ...
Outline Textview OutlineTextView Android TextView with Outline/Stroke Example ...
Android TextView属性ellipsize多行失效的解决思路... 多余文字显示省略号的常规做法 android:ellipsize="end" //省略号显示在末尾 android:ellipsize=...
翻滚的TextView 一、介绍 一个可以让数字滚动的TextView,它能兼容正数,负数,甚至是小数。翻滚的数字精度也完全可以由自己控制,每次可以变化精度的n倍,通过此功能可以实...
UI组件-TextView及其子类 TextView组件 TextView直接继承了View,它的作用就是在界面上显示文本。 代码示例 ...