前言: 有时候我们需要实现长按选择文字功能,比如阅读器一般都有这个功能,有时候某个自定义控件上可能就有这种需求,如何实现呢?正好最近还算闲,想完善一下自己写的那个轻量级的txt文件阅读器(比如这个长按选择文字的功能就想加进去)。于是花了两三天时间,实现了这个功能,效果还是不错的。

首先先看看效果图吧:

转载注务必明:http://blog.csdn.net/u014614038/article/details/74451484


授人以鱼不如授人以渔,下面具体实现原理的教程。‘

1.实现原理

原理其实也不难,简单总结就是:绘制文字时把显示的文字的坐标记录下来(记录文字的左上右上左下右下四个点坐标),作用就是为了计算滑动范围。执行了长按事件后,通过按的坐标,在当前显示的文字数据中根据点的坐标查找到按着的字,得到长按后选择的位置与文字。当执行滑动选择时,根据手指滑动的位置坐标与当前显示的文字数据匹配来确定选择的范围与文字。

2.具体实现

a.封装

为了便于操作,首先对显示可见的字符、显示的行数据进行封装。

ShowChar:

public class ShowChar {//可见字符数据封装public char chardata ;//字符数据public Boolean Selected =false;//当前字符是否被选中public Point TopLeftPosition = null;public Point TopRightPosition = null;public Point BottomLeftPosition = null;public Point BottomRightPosition = null;public float charWidth = 0;//字符宽度public int Index = 0;//当前字符位置}

ShowLine :

public class ShowLine {//显示的行数据public List<ShowChar> CharsData = null;/***@return*--------------------*TODO 获取该行的数据*--------------------*/public String getLineData(){String linedata = "";  if(CharsData==null||CharsData.size()==0) return linedata;for(ShowChar c:CharsData){linedata = linedata+c.chardata;}return linedata;}
}

说明:阅读器显示数据是一行一行的,每行都有不确定数量的字符,每个字符有自己的信息,比如字符宽度、字符在数据集合中的下标等。绘制时,通过绘制ShowLine 去绘制每行的数据。

b.数据转化

绘制前,我们需要先要把数据转化为上面封装的格式数据以便我们使用。这个要怎么做?因为我们需要将字符串转化为一行一行的数据,同时每个字符的字符宽度需要测量出来。如果对绘制比较熟悉的话,应该会知道系统有个paint.measureText可以用来测量字符的宽度,这里可以借助这个来实现测量字符的宽度,同时转化为我们想要行数据。
首先,写个方法,可以将传入的字符串转化为行数据:

 /***@param cs *@param medsurewidth 行测量的最大宽度*@param textpadding 字符间距*@param paint 测量的画笔*@return 如果cs为空或者长度为0,返回null*--------------------*TODO *--------------------*/public static BreakResult BreakText(char[] cs, float medsurewidth, float textpadding, Paint paint) {  if(cs==null||cs.length==0){return null;}BreakResult breakResult = new BreakResult();    breakResult.showChars = new ArrayList<ShowChar>();float width = 0;for (int i = 0, size = cs.length; i < size; i++) {String mesasrustr = String.valueOf(cs[i]);float charwidth = paint.measureText(mesasrustr);if (width <= medsurewidth && (width + textpadding + charwidth) > medsurewidth) {breakResult.ChartNums = i;breakResult.IsFullLine = true;return breakResult;}ShowChar showChar = new ShowChar();showChar.chardata = cs[i];showChar.charWidth = charwidth;     breakResult.showChars.add(showChar);width += charwidth + textpadding;}breakResult.ChartNums = cs.length;return breakResult;}public static BreakResult BreakText(String text, float medsurewidth, float textpadding, Paint paint) {if (TextUtils.isEmpty(text)) {int[] is = new int[2];is[0] = 0;is[1] = 0;return null;}return BreakText(text.toCharArray(), medsurewidth, textpadding, paint);}

说明: BreakResult 是对测量结果的简单封装:

public class BreakResult {public int ChartNums = 0;//测量了的字符数public Boolean IsFullLine = false;//是否满一行了public List<ShowChar> showChars = null;//测量了的字符数据public Boolean HasData() {return showChars != null && showChars.size() > 0;}
}

完成了上面的工作后,我们可以实现将我们显示的数据转化为需要的数据了。

下面是我们测试显示的字符串:

String TextData = "jEh话说天下大势,分久必合,合久必分。周末七国分争,并入于秦。及秦灭之后,楚、汉分争,又并入于汉。汉朝自高祖斩白蛇而起义,一统天下,后来光武中兴,传至献帝,遂分为三国。推其致乱之由,殆始于桓、灵二帝。桓帝禁锢善类,崇信宦官。及桓帝崩,灵帝即位,大将军窦武、太傅陈蕃共相辅佐。时有宦官曹节等弄权,窦武、陈蕃谋诛之,机事不密,反为所害,中涓自此愈横"+"建宁二年四月望日,帝御温德殿。方升座,殿角狂风骤起。只见一条大青蛇,从梁上飞将下来,蟠于椅上。帝惊倒,左右

我们需要将这段字符串转化为行数据,在初始化数据的操作,下面是初始化数据的方法initData

List<ShowLine> mLinseData = null;private void initData(int viewwidth, int viewheight) {if (mLinseData == null) {//将数据转化为行数据mLinseData = BreakText(viewwidth, viewheight);}}private List<ShowLine> BreakText(int viewwidth, int viewheight) {List<ShowLine> showLines = new ArrayList<ShowLine>();while (TextData.length() > 0) {BreakResult breakResult = TextBreakUtil.BreakText(TextData, viewwidth, 0, mPaint);if (breakResult != null && breakResult.HasData()) {ShowLine showLine = new ShowLine();showLine.CharsData = breakResult.showChars;showLines.add(showLine);} else {break;}TextData = TextData.substring(breakResult.ChartNums);}int index = 0;for (ShowLine l : showLines) {for (ShowChar c : l.CharsData) {c.Index = index++;}}return showLines;}

只要调用initData方法,我们就可以将TextData的数据转为显示的行数据Linedata集合mLinseData 。

值得注意的是,调用这个方法需求知道控件的长宽,根据view的生命周期,我们可以在onmeasures里面调用这个方法进行初始化。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int viewwidth = getMeasuredWidth();int viewheight = getMeasuredHeight();initData(viewwidth, viewheight);}

数据转化完成后,接着我们需要把数据一行一行的绘制出来:

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);  LineYPosition = TextHeight + LinePadding;//第一行显示的y坐标for (ShowLine line : mLinseData) {DrawLineText(line, canvas);//绘制每一行,并记录每个字符的坐标}}

DrawLineText方法:

private void DrawLineText(ShowLine line, Canvas canvas) {canvas.drawText(line.getLineData(), 0, LineYPosition, mPaint);float leftposition = 0;float rightposition = 0;float bottomposition = LineYPosition + mPaint.getFontMetrics().descent;for (ShowChar c : line.CharsData) {rightposition = leftposition + c.charWidth;Point tlp = new Point();c.TopLeftPosition = tlp;tlp.x = (int) leftposition;tlp.y = (int) (bottomposition - TextHeight);Point blp = new Point();c.BottomLeftPosition = blp;blp.x = (int) leftposition;blp.y = (int) bottomposition;Point trp = new Point();c.TopRightPosition = trp;trp.x = (int) rightposition;trp.y = (int) (bottomposition - TextHeight);Point brp = new Point();c.BottomRightPosition = brp;brp.x = (int) rightposition;brp.y = (int) bottomposition;leftposition = rightposition;}LineYPosition = LineYPosition + TextHeight + LinePadding;}

运行一下,目前显示效果如下:

实现这些后,接下来需要实现长按选择功能以及滑动选择文字功能。如何实现长按呢,自己写肯定可以,只是也太麻烦了,所以我们这里借助系统提供的长按事件就可以。我实现的思路是这样的,首先先将事件处理模式分四种:

private enum Mode {Normal, //正常模式PressSelectText,//长按选中文字SelectMoveForward, //向前滑动选中文字SelectMoveBack//向后滑动选中文字}

在没有做任何处理情况下是Normal模式,如果手势发生了,Down事件触发,记录当前Down的坐标,如果用户一直按着,必然触发长按事件,模式转化为PressSelectText,通过记录的Down的坐标,去数据集合中找到当前长按的字符,绘画出选择的文字的背景。

思路是这样,那么就干吧。首先注册长按事件,在初始化使注册该事件。

private void init() {mPaint = new Paint();mPaint.setAntiAlias(true);mPaint.setTextSize(29);mTextSelectPaint = new Paint();mTextSelectPaint.setAntiAlias(true);mTextSelectPaint.setTextSize(19);mTextSelectPaint.setColor(TextSelectColor);mBorderPointPaint = new Paint();mBorderPointPaint.setAntiAlias(true);mBorderPointPaint.setTextSize(19);mBorderPointPaint.setColor(BorderPointColor);FontMetrics fontMetrics = mPaint.getFontMetrics();TextHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);setOnLongClickListener(mLongClickListener);}
private OnLongClickListener mLongClickListener = new OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {if (mCurrentMode == Mode.Normal) {if (Down_X > 0 && Down_Y > 0) {// 说明还没释放,是长按事件mCurrentMode = Mode.PressSelectText;postInvalidate();//刷新}}return false;}};

这里 Down_X , Down_Y ; 初始化值都是-1,如果执行了down事件后它们肯定大于0,如果执行了Action_up事件,释放设置值为-1,只是为了判断使用而已。

然后onDraw中需要判断一下并绘制选择的文字了。

@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);LineYPosition = TextHeight + LinePadding;//第一行的y坐标for (ShowLine line : mLinseData) {DrawLineText(line, canvas);//绘制每一}if (mCurrentMode != Mode.Normal) {DrawSelectText(canvas);//如果不是正常的话,绘制选择}}
private void DrawSelectText(Canvas canvas) {if (mCurrentMode == Mode.PressSelectText) {DrawPressSelectText(canvas);//绘制长按选择的字符} else if (mCurrentMode == Mode.SelectMoveForward) {//向前滑动选择DrawMoveSelectText(canvas);//绘制滑动时选择的文字背景} else if (mCurrentMode == Mode.SelectMoveBack) {//向后滑动选择DrawMoveSelectText(canvas);//绘制滑动时选择的文字背景}}

这时如果执行了长按事件,mCurrentMode == Mode.PressSelectText,将执行绘制长按选择的字符。

//绘制长按选中的数据
private void DrawPressSelectText(Canvas canvas) {//根据按的坐标检测找到长按的字符ShowChar p = DetectPressShowChar(Down_X, Down_Y);if (p != null) {// 找到了选择的字符FirstSelectShowChar = LastSelectShowChar = p;mSelectTextPath.reset();mSelectTextPath.moveTo(p.TopLeftPosition.x, p.TopLeftPosition.y);mSelectTextPath.lineTo(p.TopRightPosition.x, p.TopRightPosition.y);mSelectTextPath.lineTo(p.BottomRightPosition.x, p.BottomRightPosition.y);mSelectTextPath.lineTo(p.BottomLeftPosition.x, p.BottomLeftPosition.y);//绘制文字背景canvas.drawPath(mSelectTextPath, mTextSelectPaint);//绘制边界的线与指示块DrawBorderPoint(canvas);}}

检测点击点所在的字符方法:

/***@param down_X2*@param down_Y2*@return*--------------------*TODO 检测获取按压坐标所在位置的字符,没有的话返回null*--------------------*/private ShowChar DetectPressShowChar(float down_X2, float down_Y2) {for (ShowLine l : mLinseData) {for (ShowChar c : l.CharsData) {if (down_Y2 > c.BottomLeftPosition.y) {break;// 说明是在下一行}if (down_X2 >= c.BottomLeftPosition.x && down_X2 <= c.BottomRightPosition.x) {return c;}}}return null;}

基本上长按事件操作都完成了,我们运行长按文字看看效果


绘制了长按选择的字符后,我们需要实现按着左右的指示块进行左右或者上下滑动去选择文字。为了便于操作,向上滑动与向下滑动都有限制滑动范围,如下图:

蓝色的区域是手指按着后触发允许滑动。按着左边的小蓝色区域,mCurrentMode == Mode.SelectMoveForward,允许向上滑动选择文字,就是手指滑动坐标滑动到黄色区域有效。按着右边的小蓝色区域,mCurrentMode == Mode.SelectMoveBack,允许向下滑动选择文字,就是手指滑动到绿色区域有效。
选择时,我们只会记录两个字符,就是选择的文字的开始字符与结束字符:

private ShowChar FirstSelectShowChar = null;
private ShowChar LastSelectShowChar = null;

注意的是当长按选择一个字符后:FirstSelectShowChar = LastSelectShowChar;

所以整个过程是:滑动时,如果按着左边的蓝色区域,将允许向前滑动,这时mCurrentMode == Mode.SelectMoveForward,向前滑动即在黄色区域滑动,这时就可以根据手指滑动坐标找到滑动后的FirstSelectShowChar ,然后刷新界面。向下滑动同理。

下面是代码实现:

先在Action_Down里判断是向下滑动还是向下滑动,如果都不是,重置,使长按选择的文字恢复原样。

case MotionEvent.ACTION_DOWN:Down_X = Tounch_X;Down_Y = Tounch_Y;if (mCurrentMode != Mode.Normal) {Boolean isTrySelectMove = CheckIfTrySelectMove(Down_X, Down_Y);if (!isTrySelectMove) {// 如果不是准备滑动选择文字,转变为正常模式,隐藏选择框mCurrentMode = Mode.Normal;invalidate();}}break;

在滑动时判断,如果是向上滑动,检测获取当前滑动时的FirstSelectShowChar ;如果是向下滑动,检测获取当前滑动时的LastSelectShowChar ,然后刷新界面。

case MotionEvent.ACTION_MOVE:if (mCurrentMode == Mode.SelectMoveForward) {if (CanMoveForward(event.getX(), event.getY())) {// 判断是否是向上移动ShowChar firstselectchar = DetectPressShowChar(event.getX(), event.getY());//获取当前滑动坐标的下的字符if (firstselectchar != null) {FirstSelectShowChar = firstselectchar;invalidate();} }} else if (mCurrentMode == Mode.SelectMoveBack) {if (CanMoveBack(event.getX(), event.getY())) {// 判断是否可以向下移动         ShowChar lastselectchar = DetectPressShowChar(event.getX(), event.getY());//获取当前滑动坐标的下的字符if (lastselectchar != null) {LastSelectShowChar = lastselectchar;invalidate();} } }break;

判断是否向上滑动方法:

private boolean CanMoveForward(float Tounchx, float Tounchy) {Path p = new Path();p.moveTo(LastSelectShowChar.TopRightPosition.x, LastSelectShowChar.TopRightPosition.y);p.lineTo(getWidth(), LastSelectShowChar.TopRightPosition.y);p.lineTo(getWidth(), 0);p.lineTo(0, 0);p.lineTo(0, LastSelectShowChar.BottomRightPosition.y);p.lineTo(LastSelectShowChar.BottomRightPosition.x, LastSelectShowChar.BottomRightPosition.y);p.lineTo(LastSelectShowChar.TopRightPosition.x, LastSelectShowChar.TopRightPosition.y);return computeRegion(p).contains((int) Tounchx, (int) Tounchy);}

判断是否向下滑动:

private boolean CanMoveBack(float Tounchx, float Tounchy) {Path p = new Path();p.moveTo(FirstSelectShowChar.TopLeftPosition.x, FirstSelectShowChar.TopLeftPosition.y);p.lineTo(getWidth(), FirstSelectShowChar.TopLeftPosition.y);p.lineTo(getWidth(), getHeight());p.lineTo(0, getHeight());p.lineTo(0, FirstSelectShowChar.BottomLeftPosition.y);p.lineTo(FirstSelectShowChar.BottomLeftPosition.x, FirstSelectShowChar.BottomLeftPosition.y);p.lineTo(FirstSelectShowChar.TopLeftPosition.x, FirstSelectShowChar.TopLeftPosition.y);return computeRegion(p).contains((int) Tounchx, (int) Tounchy);}
private Region computeRegion(Path path) {Region region = new Region();RectF f = new RectF();path.computeBounds(f, true);region.setPath(path, new Region((int) f.left, (int) f.top, (int) f.right, (int) f.bottom));return region;}

手势操作处理完成了,剩下的就是在ondraw时判断到mCurrentMode == Mode.SelectMoveForward或者mCurrentMode == Mode.SelectMoveBack绘制出选择的范围背景。

private void DrawSelectText(Canvas canvas) {if (mCurrentMode == Mode.PressSelectText) {DrawPressSelectText(canvas);//绘制长按选择的字符} else if (mCurrentMode == Mode.SelectMoveForward) {//向前滑动选择DrawMoveSelectText(canvas);//绘制滑动时选择的文字背景} else if (mCurrentMode == Mode.SelectMoveBack) {//向后滑动选择DrawMoveSelectText(canvas);//绘制滑动时选择的文字背景}}
private void DrawMoveSelectText(Canvas canvas) {if (FirstSelectShowChar == null || LastSelectShowChar == null)     return;GetSelectData();//获取选择字符的数据,转化为选择的行数据DrawSeletLines(canvas);//绘制选择的行数据DrawBorderPoint(canvas);//绘制出边界的方块或圆点}private void DrawSeletLines(Canvas canvas) DrawOaleSeletLinesBg(canvas);}private void DrawOaleSeletLinesBg(Canvas canvas) {// 绘制椭圆型的选中背景for (ShowLine l : mSelectLines) {      if (l.CharsData != null && l.CharsData.size() > 0) {        ShowChar fistchar = l.CharsData.get(0);ShowChar lastchar = l.CharsData.get(l.CharsData.size() - 1);float fw = fistchar.charWidth;float lw = lastchar.charWidth;RectF rect = new RectF(fistchar.TopLeftPosition.x, fistchar.TopLeftPosition.y,lastchar.TopRightPosition.x, lastchar.BottomRightPosition.y);canvas.drawRoundRect(rect, fw / 2,TextHeight / 2, mTextSelectPaint);}}}

基本完成了,运行一下,效果还是不错的。

转载注务必明:http://blog.csdn.net/u014614038/article/details/74451484

代码下载:github

android阅读器长按选择文字功能实现代码相关推荐

  1. 简单开发的android阅读器源码,包含了读取数据库和文件流处理功能

    原文:简单开发的android阅读器源码,包含了读取数据库和文件流处理功能 源代码下载地址:http://www.zuidaima.com/share/1838906559466496.htm 简单地 ...

  2. 发布一个练笔的 Android 阅读器,轻微仿91 Android 阅读器【后续将提供源码】

    由于是练笔Demo,所以界面上和优化方面并没有太多考虑,只是实现了一些基础功能和一些阅读器所必要的几个功能 阅读界面如下: 底下滚动条,模仿91阅读器的方式显示,按照文本长度百分比记算显示进度条,文本 ...

  3. android 仿阅读,发布一个练笔的 Android 阅读器,轻微仿91 Android 阅读器【后续将提供源码】...

    由于是练笔Demo,所以界面上和优化方面并没有太多考虑,只是实现了一些基础功能和一些阅读器所必要的几个功能 阅读界面如下: 底下滚动条,模仿91阅读器的方式显示,按照文本长度百分比记算显示进度条,文本 ...

  4. PDF阅读器和编辑器选择

    个人用过的阅读器也不多,主要是用过的PDF阅读器和编辑器主要有Adobe acrobat Pro(编辑器).Adobe reader(阅读器).PDFelement(阅读和编辑器).PDF-X-Cha ...

  5. 简单方法实现Android阅读器分页

    写了一个非常小的阅读器.在实现分页功能时,一直没有思路.后来想了一个非常特别的方法.经过测试可以完美的实现分页功能. 主要思路: 1.将文本内容填充到TextView中,调用setText一句搞定. ...

  6. Android阅读器的思路(一)

    最近工作业务中遇到了,Android阅读器的需求.面临的第一个问题就是,如何根据屏幕的大小,将一本小说分成每一页需要显示的内容.简单来说,就是把一整本小说的String变成每一页恰好可以显示的Stri ...

  7. android 阅读器放大镜,制作安卓PDF阅读器:四、实现文本选择之放大镜、长按拖动...

    看到一篇知乎上的文章,<这可能是最好用的一款 PDF 阅读.批注工具~如果他称第二,没人敢称第一!>,看了后下载试用,还好.单就触屏体验而言,没他说的那么好听. 这类国产APP的门面都是非 ...

  8. android阅读器里的 txt 文本处理分页功能的实现:

     txt 文本处理分页功能的实现: 网上找了txt文本分页,实现小说阅读器的功能找了很久找不到自己想要的,所以自己实现一个专门处理txt文本分页功能的类.有人建议用randomAccessFile 可 ...

  9. 开源能翻译英文的Android阅读器

    Filter阅读是我最近写的一个Android的阅读器,用于看英文的书(中文暂时不支持,会乱码,下一版再改). 本文章由"智障的我"原创,转载注明. http://blog.csd ...

最新文章

  1. 六轴机器人 宝元系统_庆云大国重器上线六轴智能焊接机器人,锻造高质量发展硬核...
  2. 【数据平台】Centos下仅CPU安装TensorFlow
  3. python正则group()的用法—正则提取括号内以及其他符号内内容
  4. vs android 打电话,iOS vs. Android:二者真的是在竞争吗?
  5. Excel中文字换行
  6. Qt Qwdget 汽车仪表知识点拆解4 另类进度条实现
  7. hazelcast 使用_使用Hazelcast发布和订阅
  8. iOS-property属性介绍
  9. 飞书成小米数万员工协作工具,雷军:越用越顺手
  10. paip.取回密码功能总结
  11. 计算机内加减法的溢出处理
  12. 大学生搜题软件哪个好用?2020搜题软件排行榜
  13. 【C++】1079:计算分数加减表达式的值(信息学奥赛)
  14. 433MHz无线通信
  15. mui12搭载鸿蒙,MUI系统最新资讯
  16. 服务器如何设置404错误页面
  17. Java的三种程序基本结构
  18. 粗糙集约简 程序 matlab,粗糙集属性约简matlab程序
  19. Mybatis数据持久化
  20. 信息系统项目管理师考试法律法规、行业标准、政策资料

热门文章

  1. django:session会话控制
  2. 绘画教程:如何绘画树木?插画中的树木应该怎么画?
  3. 商城项目---day07---列表页和搜索接口的实现
  4. 作业—教务系统设计文档
  5. $.ajax同步请求,异步请求
  6. 弹幕、暴漫、有声皮肤:80后眼中的“怪象”正中90后下怀?
  7. 人工智能会对人类构成威胁吗?
  8. 【计蒜客】泥塑课C++
  9. 枪火重生灵界狂潮攻略(五)元素猫流派
  10. Error setting driver on UnpooledDataSource