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

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

首先先看看效果图吧:

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

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里面调用这个方法进行初始化。

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

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

@Override protected 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() { @Override public 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中需要判断一下并绘制选择的文字了。

@Override protected 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); } } }

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

时间: 2024-10-26 02:36:50

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

Android WebView自定义长按选择实现收藏/分享选中文本功能

效果图(1.3M) 一.前言 ** 戳这里可以去DEMO,来吧 ** 相信刚接触android不久的同志们,在面对产品提出的 : "自定义WebView页面中,长按文本的弹出选项.点击选择后,分享.转发.收藏选择文本" 这样的需求时,第一反应大部分是:这是系统行为,如果实现需要在web端实现. 但是web端实现的局限性太大,曾经也有过监听系统粘贴板,在用户点击复制的时候实现其他的逻辑,但是这样用户体验不好,所以自定义WebView中长按的弹出菜单,并在点击时返回选中文本的小控件闪亮登场

android 阅读器文本对齐

问题描述 android 阅读器文本对齐 开发杂志阅读器出现疑问,想知道豆瓣阅读 是通过什么控件展示的文本,例如 : 杂志中往往包含图片和文字. 还有如何实现豆瓣阅读那种文本左右对齐?webView 虽然用text style 弄了左右对齐但是右边还是不齐,也就是说有些行长度不够. 豆瓣浏览器每一行文本 都做到对齐的 .这个效果是如何实现的 ?求解 解决方案 这个是自己画出来的,并不是某个控件,用canvas画出来的,如果要左右完全对齐那就复杂了,估计每个文字的坐标都要记录下来然后计算,就我所知

电子书-android 阅读器如何实现阅读进度的显示

问题描述 android 阅读器如何实现阅读进度的显示 如题 如何实现显示电子书阅读器阅读小说时的阅读进度,以百分比列的方式进行显示, 解决方案 特定视图模式下的总页数,和当前页数之比. 解决方案二: 参考:http://www.iteye.com/problems/64265 解决方案三: http://blog.csdn.net/ztp800201/article/details/7322110 六.使用RandomAccessFile进行文件的读写: 解决方案四: 只是显示百分比么那么只要

Android 点击生成二维码功能实现代码

先看效果: 输入内容,点击生成二维码: 点击logo图案: 代码: QRCodeUtil: package com.example.administrator.zxing; import android.graphics.Bitmap; import android.graphics.Canvas; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zx

英特尔推阅读器可朗读拍摄文字售价1499美元

英特尔阅读器约为平装书大小,可以将印刷文字数位化,然后朗诵出来.零售定价1,499美元,即日起在美国与加拿大上市,透过CTL.Don Johnston.GTSI.HumanWare等经销商贩售 腾讯科技讯11月13日消息,据台湾媒体报道,电子书市场是兵家必争之地,晶片业龙头英特尔也宣布推出自家品牌的阅读器,不过英特尔阅读器可以念给你听,对有助于阅读困难的人士. 英特尔阅读器(Intel Reader)10日亮相,约为平装书大小,可以将印刷文字数位化,然后朗诵出来.零售定价1,499美元,即日起在

用vb.net做一个RSS阅读器续--分类功能(博客园专用)

上次发布了一个简易的RSS阅读器的源代码这次给它加上了分类功能http://www.cnblogs.com/aowind/archive/2005/03/18/120942.html几有三个过程哈!对了,还有上次一位朋友把代码改写成了C#.net的http://www.cnblogs.com/79288440/archive/2005/03/17/120608.html大家也可以看看哟!历害!

Android TextView中文本点击文字跳转 (代码简单)_Android

在web页面中,有a标签的超链接实现跳转,同样在Android当中,用TextView控件来显示文字,实现它的事件来跳转. 用过微博Android手机端的朋友的都知道微博正文有时有一些高亮显示的文本,如话题.提到的人等等,当点击这些文本时会跳到另外一个页面(即另一个activity),下面就要来模仿微博的这个功能 点击#hello# 点击@人 一.新建一个名为WeiboContentTest的工程 二.在布局文件中添加一个textview 三.在mainactivity中创建该textview四

Android TextView中文本点击文字跳转 (代码简单)

在web页面中,有a标签的超链接实现跳转,同样在Android当中,用TextView控件来显示文字,实现它的事件来跳转. 用过微博Android手机端的朋友的都知道微博正文有时有一些高亮显示的文本,如话题.提到的人等等,当点击这些文本时会跳到另外一个页面(即另一个activity),下面就要来模仿微博的这个功能 点击#hello# 点击@人 一.新建一个名为WeiboContentTest的工程 二.在布局文件中添加一个textview 三.在mainactivity中创建该textview

Android调用系统自带的分享功能实例代码

实现分享功能的几个办法 1.调用系统的分享功能 2.通过第三方SDK,如ShareSDK,友盟等 3.自行使用各自平台的SDK,比如QQ,微信,微博各自的SDK 这里就记录下第一种办法. 分享文本信息 Intent textIntent = new Intent(Intent.ACTION_SEND); textIntent.setType("text/plain"); textIntent.putExtra(Intent.EXTRA_TEXT, "这是一段分享的文字&quo