package com.jcodeing.extractwordview.widget; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.Editable; import android.text.Layout; import android.text.Selection; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.widget.ListView; import android.widget.PopupWindow; import android.widget.Toast; import com.jcodeing.extractwordview.R; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The MIT License (MIT) * <p/> * Copyright (c) 2015 Jcodeing * <p/> * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * <p/> * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * <p/> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ public class EWListView extends ListView { public Activity activity; public boolean isSupportExtractWord = true; private boolean isLongPressState; Context context; // ---------三个构造----------------------------------------------$构造 // 当设置,指定样式时调用 public EWListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initialize(context); } // 布局文件初始化的时候,调用-------该构造方法,重用------------★ // 布局文件里面定义的属性都放在 AttributeSet attrs public EWListView(Context context, AttributeSet attrs) { super(context, attrs); initialize(context); } // 该方法,一般,在代码中 new 该类的时候_使用 public EWListView(Context context) { super(context); initialize(context); } // --------------------------------------------------------------$初始 private void initialize(Context context) { this.context = context; initMagnifier(); } private final int LONGPRESS = 1; private Handler mPressHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { //长按->初次启动--->显示放大镜&提词 case LONGPRESS: isLongPressState = true; Bundle data = msg.getData(); int X = data.getInt("X"); int RawX = data.getInt("RawX"); int Y = data.getInt("Y"); int RawY = data.getInt("RawY"); if (!isMoved) { et = findMotionView(X, Y); word = getSelectWord(et.getEditableText(), extractWordCurOff(et.getLayout(), et.x, et.y)); } resBitmap = getBitmap(activity, RawX - WIDTH / 2, RawY - HEIGHT / 2, WIDTH, HEIGHT); //放大镜-初次显示 calculate(RawX, RawY, MotionEvent.ACTION_DOWN); break; } } }; private int mLastMotionX, mLastMotionY; // 是否移动了 private boolean isMoved; // 移动的阈值 private static final int TOUCH_SLOP = 20; private EWListViewChildET et; private String word; @Override public boolean onTouchEvent(MotionEvent event) { if (!isSupportExtractWord) return super.onTouchEvent(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLastMotionX = x; mLastMotionY = y; isMoved = false; Message message = mPressHandler != null ? mPressHandler.obtainMessage() : new Message(); //传对象,过去后,getRawY,不是相对的Y轴. // message.obj = event; Bundle bundle = new Bundle(); bundle.putInt("X", (int) event.getX()); bundle.putInt("RawX", (int) event.getRawX()); bundle.putInt("Y", (int) event.getY()); bundle.putInt("RawY", (int) event.getRawY()); message.setData(bundle); message.what = LONGPRESS; mPressHandler.sendMessageDelayed(message, 500); break; case MotionEvent.ACTION_MOVE: if (isLongPressState) if (Math.abs(mLastMotionX - x) > TOUCH_SLOP || Math.abs(mLastMotionY - y) > TOUCH_SLOP) { //提词 et = findMotionView(x, y); Log.e("J", et.x + "-ET--move--ET-" + et.y + "cont:" + et.getEditableText().toString().charAt(0)); word = getSelectWord(et.getEditableText(), extractWordCurOff(et.getLayout(), et.x, et.y)); //放大镜 resBitmap = getBitmap(activity, (int) event.getRawX() - WIDTH / 2, (int) event.getRawY() - HEIGHT / 2, WIDTH, HEIGHT); calculate((int) event.getRawX(), (int) event.getRawY(), MotionEvent.ACTION_MOVE); return true; } if (isMoved && !isLongPressState) break; //如果移动超过阈值 if (Math.abs(mLastMotionX - x) > TOUCH_SLOP || Math.abs(mLastMotionY - y) > TOUCH_SLOP) //并且非长按状态下 if (!isLongPressState) { // 则表示移动了 isMoved = true; cleanLongPress();// 如果超出规定的移动范围--取消[长按事件] } break; case MotionEvent.ACTION_UP: if (isLongPressState) { //dis掉放大镜 removeCallbacks(showZoom); //drawLayout(); popup.dismiss(); //TODO --单词pop cleanLongPress(); if (!TextUtils.isEmpty(word) && et != null) onLongPressWord(word, et); break; } cleanLongPress();// 只要一抬起就释放[长按事件] break; case MotionEvent.ACTION_CANCEL: // 事件一取消也释放[长按事件],解决在ListView中滑动的时候长按事件的激活 cleanLongPress(); break; } return super.onTouchEvent(event); } private void cleanLongPress() { isLongPressState = false; mPressHandler.removeMessages(LONGPRESS); } private boolean calculate(int x, int y, int action) { dstPoint.set(x - WIDTH / 2, y - 3 * HEIGHT); if (y < 0) { // hide popup if out of bounds popup.dismiss(); return true; } if (action == MotionEvent.ACTION_DOWN) { removeCallbacks(showZoom); postDelayed(showZoom, DELAY_TIME); } else if (!popup.isShowing()) { showZoom.run(); } popup.update(getLeft() + dstPoint.x, getTop() + dstPoint.y, -1, -1); magnifier.invalidate(); return true; } // --------------------------------------------------------------$方法 // 单词提取 /** * @param layout * @param x 相对自己ev.getX() * @param y * @return */ 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; } 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; } public int getWordLeftIndex(Editable content, int cur) { // --left String editableText = content.toString();// getText().toString(); if (cur >= editableText.length()) return cur; int temp = 0; 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; } /** * Find the View closest to y. * * @param x * @param y * @return */ 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; } // ----------------------------------------------------$放大镜 private PopupWindow popup; private static final int WIDTH = 400; private static final int HEIGHT = 100; private static final long DELAY_TIME = 250; private Magnifier magnifier; private void initMagnifier() { BitmapDrawable resDrawable = (BitmapDrawable) context.getResources().getDrawable(R.mipmap.ic_launcher); 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); } Runnable showZoom = new Runnable() { public void run() { popup.showAtLocation(EWListView.this, Gravity.NO_GRAVITY, getLeft() + dstPoint.x, getTop() + dstPoint.y); } }; private Bitmap resBitmap; private Point dstPoint; class Magnifier extends View { private Paint mPaint; public Magnifier(Context context) { super(context); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(0xff008000); 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); } } private Bitmap bitmap;//生成的位图 //截图 /** * @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 = view.getDrawingCache(); //边界处理,否则会崩滴 if (x < 0) x = 0; if (y < 0) y = 0; if (x + width > bitmap.getWidth()) { // x = x + WIDTH / 2; // width = bitmap.getWidth() - x; //保持不改变,截取图片宽高的原则 x = bitmap.getWidth() - width; } if (y + height > bitmap.getHeight()) { // y = y + HEIGHT / 2; // height = bitmap.getHeight() - y; y = bitmap.getHeight() - height; } Rect frame = new Rect(); activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame); int toHeight = frame.top; bitmap = Bitmap.createBitmap(bitmap, x, y, width, height); view.setDrawingCacheEnabled(false); return bitmap; } public void onLongPressWord(String word, EWListViewChildET ewe) { if (!"".equals(word)) Toast.makeText(context, word, Toast.LENGTH_SHORT).show(); else { ewe.requestFocus(); ewe.setFocusable(false); // ewe.setFocusableInTouchMode(false); } } }