开源地址:https://github.com/jcodeing/ExtractWordView
效果图:
怎么实现的?
通过事件分发机制判断手指的落点处,剪取手指落点处的矩形区域,然后显示在PopupWindow里面.
第一步:自定义ListView
public class EWListView extends ListView
第二步:自定义一个Magnifier继承自view存放截图的屏幕图像(截取屏幕图像的时候应该做好边界判断控制),然后将这个view放入PopupWindow中悬浮展示。也就是那个放大镜。
private void initMagnifier() {
BitmapDrawable resDrawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.ic_launcher1);
resBitmap = resDrawable.getBitmap();
magnifier = new Magnifier(context);
//pop在宽高的基础上多加出边框的宽高
popup = new PopupWindow(magnifier, WIDTH + 2, HEIGHT + 10);
popup.setAnimationStyle(android.R.style.Animation_Toast);
dstPoint = new Point(0, 0);
}
class Magnifier extends View {
private Paint mPaint;
public Magnifier(Context context) {
super(context);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(0xffff0000);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
// draw popup
mPaint.setAlpha(255);
canvas.drawBitmap(resBitmap, 0, 0, mPaint);
canvas.restore();
//draw popup frame
mPaint.reset();//重置
mPaint.setColor(Color.LTGRAY);
mPaint.setStyle(Paint.Style.STROKE);//设置空心
mPaint.setStrokeWidth(2);
Path path1 = new Path();
path1.moveTo(0, 0);
path1.lineTo(WIDTH, 0);
path1.lineTo(WIDTH, HEIGHT);
path1.lineTo(WIDTH / 2 + 15, HEIGHT);
path1.lineTo(WIDTH / 2, HEIGHT + 10);
path1.lineTo(WIDTH / 2 - 15, HEIGHT);
path1.lineTo(0, HEIGHT);
path1.close();//封闭
canvas.drawPath(path1, mPaint);
}
}
其中resBitmap就是截取的屏幕图像,通过划线的方式画出了一个倒三角的效果。
第三步:onTouchEvent(MotionEvent event) 在MotionEvent.ACTION_MOVE,通过手指点x,y坐标计算出popup显示的坐标,在手指点的正上方,每一次MotionEvent.ACTION_MOVE事件的到来都更新一次popup的位置。
popup.update(getLeft() + dstPoint.x, getTop() + dstPoint.y, -1, -1);
基本上按照上述步骤已经完成了,只需了解原理,死磕代码会浪费比较多的事件。
接下来看一下辅助功能,截取屏幕:
/**
* @param activity
* @param x 截图起始的横坐标
* @param y 截图起始的纵坐标
* @param width
* @param height
* @return
*/
private Bitmap getBitmap(Activity activity, int x, int y, int width, int height) {
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bitmap;//生成的位图
bitmap = view.getDrawingCache();
//边界处理,否则会崩滴
if (x < 0)
x = 0;
if (y < 0)
y = 0;
if (x + width > bitmap.getWidth()) {
//保持不改变,截取图片宽高的原则
x = bitmap.getWidth() - width;
}
if (y + height > bitmap.getHeight()) {
y = bitmap.getHeight() - height;
}
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
bitmap = Bitmap.createBitmap(bitmap, x, y, width, height);
view.setDrawingCacheEnabled(false);
return bitmap;
}
大致意思是获取Activity的顶级View也就是DecorView,是一个FrameLayout。拿到这个activity的Bitmap视图,bitmap = view.getDrawingCache();基于这个activity视图,截取以x,y为左上角顶点,宽度和高度分别为with,height的矩形,bitmap =Bitmap.createBitmap(bitmap, x, y, width, height);然后将得到的bitmap赋值给srcBitmap在Magnifier的onDraw里面重绘就行OK了。
获取文字:
1.先找到自定义的EditText
EWListViewChildET findMotionView(int x, int y) {
//是否从顶部开始find提高效率
boolean isTopStart = y < getHeight() / 2;
int childCount = getChildCount();
if (childCount > 0) {
if (isTopStart) {
for (int i = 0; i < childCount; i++) {
if (!(getChildAt(i) instanceof EWListViewChildET))
return null;
EWListViewChildET v = (EWListViewChildET) getChildAt(i);
if (y <= v.getBottom()) {
//特殊处理--更新EditText--相对自己的x,y
v.y = y - v.getTop();
v.x = x;
Log.e("J", "ET-->y::" + y + "--updata->" + v.y);
return v;
}
}
} else {
for (int i = childCount - 1; i >= 0; i--) {
if (!(getChildAt(i) instanceof EWListViewChildET))
return null;
EWListViewChildET v = (EWListViewChildET) getChildAt(i);
if (y >= v.getTop()) {
v.y = y - v.getTop();
v.x = x;
Log.e("J", "ET-->y::" + y + "--updata->" + v.y);
return v;
}
}
}
}
return null;
}
类似于二分查找的方式,先通过isTopStart来判断是从上往下查找还是从下往上查找(提高查找效率),然后就是遍历每个item,看x,y有没有落到他的区域内,如果是就返回该自定义EditText.
2.得到按压的文字
1)首先得到按压位置的偏移,相对于本item本身,这个x,y是在findMotionView里面赋值的。
public int extractWordCurOff(Layout layout, int x, int y) {
int line;
line = layout
.getLineForVertical(getScrollY() + y - 10);
int curOff = layout.getOffsetForHorizontal(line, x);
return curOff;
}
2)通过手指位置相对于item的偏移计算出当前按压的文字:
public String getSelectWord(Editable content, int curOff) {
String word = "";
int start = getWordLeftIndex(content, curOff);
int end = getWordRightIndex(content, curOff);
if (start >= 0 && end >= 0) {
word = content.subSequence(start, end).toString();
if (!"".equals(word)) {
// setFocusable(false);
et.setFocusableInTouchMode(true);
et.requestFocus();
Selection.setSelection(content, start, end);// 设置当前具有焦点的文本字段的选择范围,当前文本必须具有焦点,否则此方法无效
}
}
return word;
}
3)计算出截取问题的其实位置
截取的开始位置索引:
public int getWordLeftIndex(Editable content, int cur) {
// --left
String editableText = content.toString();// getText().toString();
if (cur >= editableText.length())
return cur;
int temp = 0;
//单词的长度一般不会超过20位,将temp索引往前推移20位
if (cur >= 20)
temp = cur - 20;
//正则表达式匹配单词开头
Pattern pattern = Pattern.compile("[^'A-Za-z]");
Matcher m = pattern.matcher(editableText.charAt(cur) + "");
//找到就返回索引
if (m.find())
return cur;
String text = editableText.subSequence(temp, cur).toString();
//没有找到就,从索引处往前面扫描,知道找到匹配的为止返回该处的索引
int i = text.length() - 1;
for (; i >= 0; i--) {
Matcher mm = pattern.matcher(text.charAt(i) + "");
if (mm.find())
break;
}
int start = i + 1;
start = cur - (text.length() - start);
return start;
}
不清楚的地方可以看注释
截取结束位置索引:
public int getWordRightIndex(Editable content, int cur) {
// --right
String editableText = content.toString();
if (cur >= editableText.length())
return cur;
int templ = editableText.length();
if (cur <= templ - 20)
templ = cur + 20;
Pattern pattern = Pattern.compile("[^'A-Za-z]");
Matcher m = pattern.matcher(editableText.charAt(cur) + "");
if (m.find())
return cur;
String text1 = editableText.subSequence(cur, templ).toString();
int i = 0;
for (; i < text1.length(); i++) {
Matcher mm = pattern.matcher(text1.charAt(i) + "");
if (mm.find())
break;
}
int end = i;
end = cur + end;
return end;
}
原理跟寻找开始位置一样。
然后就可以根据开始位置和结束位置截取字符串了:
word = content.subSequence(start, end).toString()
OK,结束。