2 * Copyright (C) 2012 - 2014 Brandon Tate, bossturbo
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.bossturban.webviewmarker;
19 import org.json.JSONException;
20 import org.json.JSONObject;
22 import com.blahti.drag.DragController;
23 import com.blahti.drag.DragController.DragBehavior;
24 import com.blahti.drag.DragLayer;
25 import com.blahti.drag.DragListener;
26 import com.blahti.drag.DragSource;
27 import com.blahti.drag.MyAbsoluteLayout;
28 import com.bossturban.webviewmarker.R;
30 import android.annotation.SuppressLint;
31 import android.app.Activity;
32 import android.content.Context;
33 import android.graphics.Rect;
34 import android.os.Build;
35 import android.util.DisplayMetrics;
36 import android.util.Log;
37 import android.view.Display;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.View.OnTouchListener;
42 import android.view.View.OnLongClickListener;
43 import android.view.ViewGroup;
44 import android.view.WindowManager;
45 import android.webkit.WebSettings;
46 import android.webkit.WebView;
47 import android.widget.ImageView;
49 import java.util.Locale;
51 @SuppressLint("DefaultLocale")
52 public class TextSelectionSupport implements TextSelectionControlListener, OnTouchListener, OnLongClickListener, DragListener {
53 public interface SelectionListener {
54 void startSelection();
55 void selectionChanged(String text);
59 private enum HandleType {
64 private static final String TAG = "SelectionSupport";
65 private static final float CENTERING_SHORTER_MARGIN_RATIO = 12.0f / 48.0f;
66 private static final int JACK_UP_PADDING = 2;
67 private static final int SCROLLING_THRESHOLD = 10;
69 private Activity mActivity;
70 private WebView mWebView;
71 private SelectionListener mSelectionListener;
72 private DragLayer mSelectionDragLayer;
73 private DragController mDragController;
74 private ImageView mStartSelectionHandle;
75 private ImageView mEndSelectionHandle;
76 private Rect mSelectionBounds = null;
77 private final Rect mSelectionBoundsTemp = new Rect();
78 private TextSelectionController mSelectionController = null;
79 private int mContentWidth = 0;
80 private HandleType mLastTouchedSelectionHandle = HandleType.UNKNOWN;
81 private boolean mScrolling = false;
82 private float mScrollDiffY = 0;
83 private float mLastTouchY = 0;
84 private float mScrollDiffX = 0;
85 private float mLastTouchX = 0;
86 private float mScale = 1.0f;
88 private Runnable mStartSelectionModeHandler = new Runnable() {
90 if (mSelectionBounds != null) {
91 mWebView.addView(mSelectionDragLayer);
92 drawSelectionHandles();
93 final int contentHeight = (int)Math.ceil(getDensityDependentValue(mWebView.getContentHeight(), mActivity));
94 final int contentWidth = mWebView.getWidth();
95 ViewGroup.LayoutParams layerParams = mSelectionDragLayer.getLayoutParams();
96 layerParams.height = contentHeight;
97 layerParams.width = Math.max(contentWidth, mContentWidth);
98 mSelectionDragLayer.setLayoutParams(layerParams);
99 if (mSelectionListener != null) {
100 mSelectionListener.startSelection();
105 private Runnable endSelectionModeHandler = new Runnable(){
107 mWebView.removeView(mSelectionDragLayer);
108 mSelectionBounds = null;
109 mLastTouchedSelectionHandle = HandleType.UNKNOWN;
110 mWebView.loadUrl("javascript: android.selection.clearSelection();");
111 if (mSelectionListener != null) {
112 mSelectionListener.endSelection();
117 private TextSelectionSupport(Activity activity, WebView webview) {
118 mActivity = activity;
121 public static TextSelectionSupport support(Activity activity, WebView webview) {
122 final TextSelectionSupport selectionSupport = new TextSelectionSupport(activity, webview);
123 selectionSupport.setup();
124 return selectionSupport;
127 public void onScaleChanged(float oldScale, float newScale) {
130 public void setSelectionListener(SelectionListener listener) {
131 mSelectionListener = listener;
135 // Interfaces of TextSelectionControlListener
138 public void jsError(String error) {
139 Log.e(TAG, "JSError: " + error);
142 public void jsLog(String message) {
143 Log.d(TAG, "JSLog: " + message);
146 public void startSelectionMode() {
147 mActivity.runOnUiThread(mStartSelectionModeHandler);
150 public void endSelectionMode() {
151 mActivity.runOnUiThread(endSelectionModeHandler);
154 public void setContentWidth(float contentWidth){
155 mContentWidth = (int)getDensityDependentValue(contentWidth, mActivity);
158 public void selectionChanged(String range, String text, String handleBounds, boolean isReallyChanged){
159 final Context ctx = mActivity;
161 final JSONObject selectionBoundsObject = new JSONObject(handleBounds);
162 final float scale = getDensityIndependentValue(mScale, ctx);
163 Rect rect = mSelectionBoundsTemp;
164 rect.left = (int)(getDensityDependentValue(selectionBoundsObject.getInt("left"), ctx) * scale);
165 rect.top = (int)(getDensityDependentValue(selectionBoundsObject.getInt("top"), ctx) * scale);
166 rect.right = (int)(getDensityDependentValue(selectionBoundsObject.getInt("right"), ctx) * scale);
167 rect.bottom = (int)(getDensityDependentValue(selectionBoundsObject.getInt("bottom"), ctx) * scale);
168 mSelectionBounds = rect;
169 if (!isInSelectionMode()){
170 startSelectionMode();
172 drawSelectionHandles();
173 if (mSelectionListener != null && isReallyChanged) {
174 mSelectionListener.selectionChanged(text);
177 catch (JSONException e) {
183 // Interface of OnTouchListener
186 public boolean onTouch(View v, MotionEvent event) {
187 final Context ctx = mActivity;
188 float xPoint = getDensityIndependentValue(event.getX(), ctx) / getDensityIndependentValue(mScale, ctx);
189 float yPoint = getDensityIndependentValue(event.getY(), ctx) / getDensityIndependentValue(mScale, ctx);
191 switch (event.getAction()) {
192 case MotionEvent.ACTION_DOWN:
193 // Essential to add Locale.US parameter to String.format, else does not work on systems
194 // with default locale different, with other floating point notations, e.g. comma instead
196 final String startTouchUrl = String.format(Locale.US, "javascript:android.selection.startTouch(%f, %f);", xPoint, yPoint);
197 mLastTouchX = xPoint;
198 mLastTouchY = yPoint;
199 mWebView.loadUrl(startTouchUrl);
201 case MotionEvent.ACTION_UP:
205 // Fixes 4.4 double selection
206 // See: http://stackoverflow.com/questions/20391783/how-to-avoid-default-selection-on-long-press-in-android-kitkat-4-4
207 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
215 // Fixes 4.4 double selection
216 // See: http://stackoverflow.com/questions/20391783/how-to-avoid-default-selection-on-long-press-in-android-kitkat-4-4
217 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && isInSelectionMode()) {
221 case MotionEvent.ACTION_MOVE:
222 mScrollDiffX += (xPoint - mLastTouchX);
223 mScrollDiffY += (yPoint - mLastTouchY);
224 mLastTouchX = xPoint;
225 mLastTouchY = yPoint;
226 if (Math.abs(mScrollDiffX) > SCROLLING_THRESHOLD || Math.abs(mScrollDiffY) > SCROLLING_THRESHOLD) {
235 // Interface of OnLongClickListener
238 public boolean onLongClick(View v){
239 if (!isInSelectionMode()) {
240 mWebView.loadUrl("javascript:android.selection.longTouch();");
247 // Interface of DragListener
250 public void onDragStart(DragSource source, Object info, DragBehavior dragBehavior) {
253 public void onDragEnd() {
254 mActivity.runOnUiThread(new Runnable() {
257 MyAbsoluteLayout.LayoutParams startHandleParams = (MyAbsoluteLayout.LayoutParams)mStartSelectionHandle.getLayoutParams();
258 MyAbsoluteLayout.LayoutParams endHandleParams = (MyAbsoluteLayout.LayoutParams)mEndSelectionHandle.getLayoutParams();
259 final Context ctx = mActivity;
260 final float scale = getDensityIndependentValue(mScale, ctx);
261 float startX = startHandleParams.x - mWebView.getScrollX() + mStartSelectionHandle.getWidth() * (1 - CENTERING_SHORTER_MARGIN_RATIO);
262 float startY = startHandleParams.y - mWebView.getScrollY() - JACK_UP_PADDING;
263 float endX = endHandleParams.x - mWebView.getScrollX() + mEndSelectionHandle.getWidth() * CENTERING_SHORTER_MARGIN_RATIO;
264 float endY = endHandleParams.y - mWebView.getScrollY() - JACK_UP_PADDING;
265 startX = getDensityIndependentValue(startX, ctx) / scale;
266 startY = getDensityIndependentValue(startY, ctx) / scale;
267 endX = getDensityIndependentValue(endX, ctx) / scale;
268 endY = getDensityIndependentValue(endY, ctx) / scale;
269 if (mLastTouchedSelectionHandle == HandleType.START && startX > 0 && startY > 0){
270 String saveStartString = String.format(Locale.US, "javascript: android.selection.setStartPos(%f, %f);", startX, startY);
271 mWebView.loadUrl(saveStartString);
273 else if (mLastTouchedSelectionHandle == HandleType.END && endX > 0 && endY > 0){
274 String saveEndString = String.format(Locale.US, "javascript: android.selection.setEndPos(%f, %f);", endX, endY);
275 mWebView.loadUrl(saveEndString);
278 mWebView.loadUrl("javascript: android.selection.restoreStartEndPos();");
284 @SuppressLint("SetJavaScriptEnabled")
285 private void setup(){
286 mScale = mActivity.getResources().getDisplayMetrics().density;
287 mWebView.setOnLongClickListener(this);
288 mWebView.setOnTouchListener(this);
289 final WebSettings settings = mWebView.getSettings();
290 settings.setJavaScriptEnabled(true);
291 settings.setJavaScriptCanOpenWindowsAutomatically(true);
292 mSelectionController = new TextSelectionController(this);
293 mWebView.addJavascriptInterface(mSelectionController, TextSelectionController.INTERFACE_NAME);
294 createSelectionLayer(mActivity);
296 private void createSelectionLayer(Context context){
297 final LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
298 mSelectionDragLayer = (DragLayer)inflater.inflate(R.layout.selection_drag_layer, null);
299 mDragController = new DragController(context);
300 mDragController.setDragListener(this);
301 mDragController.addDropTarget(mSelectionDragLayer);
302 mSelectionDragLayer.setDragController(mDragController);
303 mStartSelectionHandle = (ImageView)mSelectionDragLayer.findViewById(R.id.startHandle);
304 mStartSelectionHandle.setTag(HandleType.START);
305 mEndSelectionHandle = (ImageView)mSelectionDragLayer.findViewById(R.id.endHandle);
306 mEndSelectionHandle.setTag(HandleType.END);
307 final OnTouchListener handleTouchListener = new OnTouchListener(){
309 public boolean onTouch(View v, MotionEvent event) {
310 boolean handledHere = false;
311 if (event.getAction() == MotionEvent.ACTION_DOWN) {
312 handledHere = startDrag(v);
313 mLastTouchedSelectionHandle = (HandleType)v.getTag();
318 mStartSelectionHandle.setOnTouchListener(handleTouchListener);
319 mEndSelectionHandle.setOnTouchListener(handleTouchListener);
321 private void drawSelectionHandles(){
322 mActivity.runOnUiThread(drawSelectionHandlesHandler);
324 private Runnable drawSelectionHandlesHandler = new Runnable(){
326 MyAbsoluteLayout.LayoutParams startParams = (com.blahti.drag.MyAbsoluteLayout.LayoutParams)mStartSelectionHandle.getLayoutParams();
327 final int startWidth = mStartSelectionHandle.getDrawable().getIntrinsicWidth();
328 startParams.x = (int)(mSelectionBounds.left - startWidth * (1.0f - CENTERING_SHORTER_MARGIN_RATIO));
329 startParams.y = (int)(mSelectionBounds.top);
330 final int startMinLeft = -(int)(startWidth * (1 - CENTERING_SHORTER_MARGIN_RATIO));
331 startParams.x = (startParams.x < startMinLeft) ? startMinLeft : startParams.x;
332 startParams.y = (startParams.y < 0) ? 0 : startParams.y;
333 mStartSelectionHandle.setLayoutParams(startParams);
335 MyAbsoluteLayout.LayoutParams endParams = (com.blahti.drag.MyAbsoluteLayout.LayoutParams)mEndSelectionHandle.getLayoutParams();
336 final int endWidth = mEndSelectionHandle.getDrawable().getIntrinsicWidth();
337 endParams.x = (int) (mSelectionBounds.right - endWidth * CENTERING_SHORTER_MARGIN_RATIO);
338 endParams.y = (int) (mSelectionBounds.bottom);
339 final int endMinLeft = -(int)(endWidth * (1- CENTERING_SHORTER_MARGIN_RATIO));
340 endParams.x = (endParams.x < endMinLeft) ? endMinLeft : endParams.x;
341 endParams.y = (endParams.y < 0) ? 0 : endParams.y;
342 mEndSelectionHandle.setLayoutParams(endParams);
346 private boolean isInSelectionMode(){
347 return this.mSelectionDragLayer.getParent() != null;
349 private boolean startDrag(View v) {
351 mDragController.startDrag(v, mSelectionDragLayer, dragInfo, DragBehavior.MOVE);
355 private float getDensityDependentValue(float val, Context ctx){
356 Display display = ((WindowManager)ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
357 DisplayMetrics metrics = new DisplayMetrics();
358 display.getMetrics(metrics);
359 return val * (metrics.densityDpi / 160f);
361 private float getDensityIndependentValue(float val, Context ctx){
362 Display display = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
363 DisplayMetrics metrics = new DisplayMetrics();
364 display.getMetrics(metrics);
365 return val / (metrics.densityDpi / 160f);