前言: 有時候我們需要實現長按選擇文字功能,比如閱讀器一般都有這個功能,有時候某個自定義控件上可能就有這種需求,如何實現呢?正好最近還算閒,想完善一下自己寫的那個輕量級的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裏面調用這個方法進行初始化。
@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);
}
}
}
基本完成了,運行一下,效果還是不錯的。
轉載注務必明:http://blog.csdn.net/u014614038/article/details/74451484
代碼下載:github