Java tutorial
/* * Copyright (C) 2012 Brandon Tate * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.roger.lineselectionwebview; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.Display; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.WindowManager; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.Toast; import com.roger.lineselectionwebview.drag.DragController; import com.roger.lineselectionwebview.drag.DragLayer; import com.roger.lineselectionwebview.drag.DragListener; import com.roger.lineselectionwebview.drag.DragSource; import com.roger.lineselectionwebview.drag.MyAbsoluteLayout; import com.roger.lineselectionwebview.util.ActionItem; import com.roger.lineselectionwebview.util.QuickAction; import com.roger.lineselectionwebview.util.QuickAction.OnDismissListener; /** * Webview subclass that hijacks web content selection. * * @author Brandon Tate */ public class LSWebView extends WebView implements TextSelectionJavascriptInterfaceListener, OnTouchListener, OnLongClickListener, OnDismissListener, DragListener { /** Context. */ protected Context mContext; /** The context menu. */ protected QuickAction mContextMenu; /** The drag layer for selection. */ protected DragLayer mSelectionDragLayer; /** The drag controller for selection. */ protected DragController mDragController; /** The selection bounds. */ protected Rect mSelectionBounds = null; /** The previously selected region. */ protected Region mLastSelectedRegion = null; /** The selected range. */ protected String mSelectedRange = ""; /** The selected text. */ protected String mSelectedText = ""; /** Javascript interface for catching text selection. */ protected TextSelectionJavascriptInterface mTextSelectionJSInterface = null; /** Selection mode flag. */ protected boolean mInSelectionMode = false; /** Flag for dragging. */ protected boolean mDragging = false; /** Flag to stop from showing context menu twice. */ protected boolean mContextMenuVisible = false; /** The current content width. */ protected int mContentWidth = 0; /** The current scale of the web view. */ protected float mCurrentScale = 1.0f; // ***************************************************** // * // * Selection Handles // * // ***************************************************** /** The start selection handle. */ protected ImageView mStartSelectionHandle; /** the end selection handle. */ protected ImageView mEndSelectionHandle; /** Identifier for the selection start handle. */ protected final int SELECTION_START_HANDLE = 0; /** Identifier for the selection end handle. */ protected final int SELECTION_END_HANDLE = 1; /** Last touched selection handle. */ protected int mLastTouchedSelectionHandle = -1; private List<Rect> rectList; /** * */ private String selectContext; /** * view */ private LineView lineView; /** * ? */ private MyAbsoluteLayout myLayout; /** * WebView? */ private int scrollY; /** * ?? */ public static final String SELECT_SPLIT = "~d@@g~"; /** * ? */ private boolean isLineMode = false;; public LSWebView(Context context) { super(context); mContext = context; setup(context); } public LSWebView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; setup(context); } public LSWebView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; setup(context); } // ***************************************************** // * // * Touch Listeners // * // ***************************************************** private boolean mScrolling = false; private float mLastTouchY = 0; private float mLastTouchX = 0; @SuppressWarnings("deprecation") @Override public boolean onTouch(View v, MotionEvent event) { if (!isLineMode) { return false; } @SuppressWarnings("deprecation") float xPoint = getDensityIndependentValue(event.getX(), mContext) / getDensityIndependentValue(getScale(), mContext); float yPoint = getDensityIndependentValue(event.getY(), mContext) / getDensityIndependentValue(getScale(), mContext); if (event.getAction() == MotionEvent.ACTION_DOWN) { String startTouchUrl = String.format(Locale.US, "javascript:android.selection.startTouch(%f, %f);", xPoint, yPoint); mLastTouchX = xPoint; mLastTouchY = yPoint; loadUrl(startTouchUrl); // Tell the javascript to handle this if not in selection mode if (!isInSelectionMode()) { loadUrl("javascript:android.selection.longTouch();"); mScrolling = true; } // Flag scrolling for first touch // mScrolling = !isInSelectionMode(); } else if (event.getAction() == MotionEvent.ACTION_UP) { // Check for scrolling flag if (!mScrolling) { mScrolling = false; endSelectionMode(); return false; } mScrolling = false; // Fixes 4.4 double selection return true; } else if (event.getAction() == MotionEvent.ACTION_MOVE) { mLastTouchX = xPoint; mLastTouchY = yPoint; } // If this is in selection mode, then nothing else should handle this // touch return false; } @Override public boolean onLongClick(View v) { if (!isLineMode) { return false; } // Don't let the webview handle it return true; } // ***************************************************** // * // * Setup // * // ***************************************************** /** * Setups up the web view. * * @param context */ @SuppressLint("SetJavaScriptEnabled") protected void setup(Context context) { // On Touch Listener setOnLongClickListener(this); setOnTouchListener(this); // Webview setup getSettings().setJavaScriptEnabled(true); getSettings().setJavaScriptCanOpenWindowsAutomatically(true); getSettings().setPluginState(WebSettings.PluginState.ON); // getSettings().setBuiltInZoomControls(true); // Webview client. setWebViewClient(new WebViewClient() { // This is how it is supposed to work, so I'll leave it in, but this // doesn't get called on pinch // So for now I have to use deprecated getScale method. @Override public void onScaleChanged(WebView view, float oldScale, float newScale) { super.onScaleChanged(view, oldScale, newScale); mCurrentScale = newScale; } }); // Zoom out fully // getSettings().setLoadWithOverviewMode(true); // getSettings().setUseWideViewPort(true); // Javascript interfaces mTextSelectionJSInterface = new TextSelectionJavascriptInterface(context, this); addJavascriptInterface(mTextSelectionJSInterface, mTextSelectionJSInterface.getInterfaceName()); // Create the selection handles createSelectionLayer(context); // Set to the empty region Region region = new Region(); region.setEmpty(); mLastSelectedRegion = region; // Load up the android asset file // String filePath = "file:///android_asset/content.html"; // Load the url // this.loadUrl(filePath); } // ***************************************************** // * // * Selection Layer Handling // * // ***************************************************** /** * Creates the selection layer. * * @param context */ protected void createSelectionLayer(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mSelectionDragLayer = (DragLayer) inflater.inflate(R.layout.selection_drag_layer, null); // Make sure it's filling parent mDragController = new DragController(context); mDragController.setDragListener(this); mDragController.addDropTarget(mSelectionDragLayer); mSelectionDragLayer.setDragController(mDragController); mStartSelectionHandle = (ImageView) mSelectionDragLayer.findViewById(R.id.startHandle); mStartSelectionHandle.setTag(new Integer(SELECTION_START_HANDLE)); mEndSelectionHandle = (ImageView) mSelectionDragLayer.findViewById(R.id.endHandle); mEndSelectionHandle.setTag(new Integer(SELECTION_END_HANDLE)); OnTouchListener handleTouchListener = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { boolean handledHere = false; final int action = event.getAction(); // Down event starts drag for handle. if (action == MotionEvent.ACTION_DOWN) { handledHere = startDrag(v); mLastTouchedSelectionHandle = (Integer) v.getTag(); } return handledHere; } }; mStartSelectionHandle.setOnTouchListener(handleTouchListener); mEndSelectionHandle.setOnTouchListener(handleTouchListener); } /** * Starts selection mode on the UI thread */ private Handler startSelectionModeHandler = new Handler() { public void handleMessage(Message m) { if (mSelectionBounds == null) return; addView(mSelectionDragLayer); drawSelectionHandles(); int contentHeight = (int) Math.ceil(getDensityDependentValue(getContentHeight(), mContext)); // Update Layout Params ViewGroup.LayoutParams layerParams = mSelectionDragLayer.getLayoutParams(); layerParams.height = contentHeight; layerParams.width = mContentWidth; mSelectionDragLayer.setLayoutParams(layerParams); } }; /** * Starts selection mode. * */ public void startSelectionMode() { startSelectionModeHandler.sendEmptyMessage(0); } // Ends selection mode on the UI thread private Handler endSelectionModeHandler = new Handler() { public void handleMessage(Message m) { if (getParent() != null && mContextMenu != null && mContextMenuVisible) { // This will throw an error if the webview is being redrawn. // No error handling needed, just need to stop the crash. try { mContextMenu.dismiss(); } catch (Exception e) { } } mSelectionBounds = null; mLastTouchedSelectionHandle = -1; loadUrl("javascript: android.selection.clearSelection();"); removeView(mSelectionDragLayer); } }; /** * Ends selection mode. */ public void endSelectionMode() { endSelectionModeHandler.sendEmptyMessage(0); } /** * Calls the handler for drawing the selection handles. */ private void drawSelectionHandles() { drawSelectionHandlesHandler.sendEmptyMessage(0); } /** * Handler for drawing the selection handles on the UI thread. */ private Handler drawSelectionHandlesHandler = new Handler() { public void handleMessage(Message m) { MyAbsoluteLayout.LayoutParams startParams = (MyAbsoluteLayout.LayoutParams) mStartSelectionHandle .getLayoutParams(); startParams.x = (int) (mSelectionBounds.left - mStartSelectionHandle.getDrawable().getIntrinsicWidth()); startParams.y = (int) (mSelectionBounds.top); // Stay on screen. startParams.x = (startParams.x < 0) ? 0 : startParams.x; startParams.y = (startParams.y < 0) ? 0 : startParams.y; mStartSelectionHandle.setLayoutParams(startParams); MyAbsoluteLayout.LayoutParams endParams = (MyAbsoluteLayout.LayoutParams) mEndSelectionHandle .getLayoutParams(); endParams.x = (int) mSelectionBounds.right; endParams.y = (int) mSelectionBounds.bottom; // Stay on screen endParams.x = (endParams.x < 0) ? 0 : endParams.x; endParams.y = (endParams.y < 0) ? 0 : endParams.y; mEndSelectionHandle.setLayoutParams(endParams); } }; /** * Checks to see if this view is in selection mode. * * @return */ public boolean isInSelectionMode() { return mSelectionDragLayer.getParent() != null; } /** * Checks to see if the view is currently dragging. * * @return */ public boolean isDragging() { return mDragging; } // ***************************************************** // * // * DragListener Methods // * // ***************************************************** /** * Start dragging a view. * */ private boolean startDrag(View v) { // Let the DragController initiate a drag-drop sequence. // I use the dragInfo to pass along the object being dragged. // I'm not sure how the Launcher designers do this. mDragging = true; Object dragInfo = v; mDragController.startDrag(v, mSelectionDragLayer, dragInfo, DragController.DRAG_ACTION_MOVE); return true; } @Override public void onDragStart(DragSource source, Object info, int dragAction) { // TODO Auto-generated method stub } @Override public void onDrag() { // TODO Auto-generated method stub } @Override public void onDragEnd() { // TODO Auto-generated method stub MyAbsoluteLayout.LayoutParams startHandleParams = (MyAbsoluteLayout.LayoutParams) mStartSelectionHandle .getLayoutParams(); MyAbsoluteLayout.LayoutParams endHandleParams = (MyAbsoluteLayout.LayoutParams) mEndSelectionHandle .getLayoutParams(); float scale = getDensityIndependentValue(getScale(), mContext); float startX = startHandleParams.x - getScrollX(); float startY = startHandleParams.y - getScrollY(); float endX = endHandleParams.x - getScrollX(); float endY = endHandleParams.y - getScrollY(); startX = getDensityIndependentValue(startX, mContext) / scale; startY = getDensityIndependentValue(startY, mContext) / scale; endX = getDensityIndependentValue(endX, mContext) / scale; endY = getDensityIndependentValue(endY, mContext) / scale; if (startY <= 15) { startY = 15; } if (endY <= 15) { endY = 15; } System.out.println("onDragEnd startY = " + startY + ", endY = " + endY); if (mLastTouchedSelectionHandle == SELECTION_START_HANDLE && startX > 0 && startY > 0) { String saveStartString = String.format(Locale.US, "javascript: android.selection.setStartPos(%f, %f);", startX, startY); loadUrl(saveStartString); } if (endY <= 0) { endY = 10; } if (mLastTouchedSelectionHandle == SELECTION_END_HANDLE && endX > 0 && endY > 0) { String saveEndString = String.format(Locale.US, "javascript: android.selection.setEndPos(%f, %f);", endX, endY); loadUrl(saveEndString); } mDragging = false; } // ***************************************************** // * // * Context Menu Creation // * // ***************************************************** /** * Shows the context menu using the given region as an anchor point. * * @param displayRect */ protected void showContextMenu(Rect displayRect) { // Don't show this twice if (mContextMenuVisible) { return; } // Don't use empty rect // if(displayRect.isEmpty()){ if (displayRect.right <= displayRect.left) { return; } // Copy action item ActionItem buttonOne = new ActionItem(); buttonOne.setTitle("Selected"); buttonOne.setActionId(1); // Highlight action item // ActionItem buttonTwo = new ActionItem(); // // buttonTwo.setTitle("??"); // buttonTwo.setActionId(2); // buttonTwo.setIcon(getResources().getDrawable(R.drawable.menu_info)); // ActionItem buttonThree = new ActionItem(); // buttonThree.setTitle("Button 3"); // buttonThree.setActionId(3); // buttonThree.setIcon(getResources().getDrawable(R.drawable.menu_eraser)); // The action menu mContextMenu = new QuickAction(getContext()); mContextMenu.setOnDismissListener(this); // Add buttons mContextMenu.addActionItem(buttonOne); // mContextMenu.addActionItem(buttonTwo); // mContextMenu.addActionItem(buttonThree); // setup the action item click listener mContextMenu.setOnActionItemClickListener(new QuickAction.OnActionItemClickListener() { @Override public void onItemClick(QuickAction source, int pos, int actionId) { // TODO Auto-generated method stub if (actionId == 1) { // Do Button 1 stuff // mdf by zz drawLayout(); endSelectionMode(); if (onTextSelectListener != null) { onTextSelectListener.select(selectContext.split(SELECT_SPLIT)[0]); } } mContextMenuVisible = false; } }); mContextMenuVisible = true; mContextMenu.show(this, displayRect); } /** * webview */ private void drawLayout() { // key=????? if (rectList.size() > 1) { Rect rectStart = rectList.get(0); Rect rectEnd = rectList.get(rectList.size() - 1); selectContext = selectContext + SELECT_SPLIT + rectStart.left + rectEnd.right; } else { Rect rect = rectList.get(0); selectContext = selectContext + SELECT_SPLIT + rect.left + rect.right; } if (scrollY > 0) { for (Rect rect : rectList) { rect.bottom = rect.bottom + scrollY; } } if (!lineView.exist(selectContext)) { // drawLine(); // drawImage(); } } /** * ? */ private void drawImage() { int size = rectList.size(); Rect endRect = rectList.get(size - 1); Rect realEndRect = endRect; // ?left???,,? if (size > 2) { Rect endPreRect = rectList.get(size - 2); if (endRect.left == endPreRect.left) { if (endRect.width() > endPreRect.width()) { realEndRect = endPreRect; } else { realEndRect = endRect; } } } Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.del); ImageView imageView = new ImageView(mContext); imageView.setImageBitmap(bitmap); imageView.setScaleType(ScaleType.CENTER); imageView.setTag(selectContext); MyAbsoluteLayout.LayoutParams lp = new MyAbsoluteLayout.LayoutParams( MyAbsoluteLayout.LayoutParams.WRAP_CONTENT, MyAbsoluteLayout.LayoutParams.WRAP_CONTENT, realEndRect.right, realEndRect.bottom); imageView.setLayoutParams(lp); imageView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String tag = (String) v.getTag(); lineView.remove(tag); myLayout.removeView(v); if (onTextSelectListener != null) { onTextSelectListener.cancle(tag.split(SELECT_SPLIT)[0]); } } }); myLayout.addView(imageView, lp); } /** * */ private void drawLine() { lineView.add(selectContext, rectList); } /** * ? * * @param container webView */ public void openLineMode(ViewGroup container) { this.myLayout = (MyAbsoluteLayout) LayoutInflater.from(mContext).inflate(R.layout.line, null); LayoutParams mLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 0, 0); myLayout.setLayoutParams(mLayoutParams); container.addView(myLayout); lineView = (LineView) myLayout.findViewById(R.id.lineView); isLineMode = true; } // ***************************************************** // * // * OnDismiss Listener // * // ***************************************************** /** * Clears the selection when the context menu is dismissed. */ public void onDismiss() { // clearSelection(); mContextMenuVisible = false; } // ***************************************************** // * // * Text Selection Javascript Interface Listener // * // ***************************************************** /** * Shows/updates the context menu based on the range * * @param error */ public void tsjiJSError(String error) { } /** * The user has started dragging the selection handles. */ public void tsjiStartSelectionMode() { startSelectionMode(); } /** * The user has stopped dragging the selection handles. */ public void tsjiEndSelectionMode() { endSelectionMode(); } /** * The selection has changed * * @param range * @param text * @param handleBounds * @param menuBounds */ public void tsjiSelectionChanged(String range, String text, String handleBounds, String menuBounds) { handleSelection(range, text, handleBounds); Rect displayRect = getContextMenuBounds(menuBounds); if (displayRect != null) // This will send the menu rect showContextMenu(displayRect); } /** * Receives the content width for the page. */ public void tsjiSetContentWidth(float contentWidth) { mContentWidth = (int) getDensityDependentValue(contentWidth, mContext); } // ***************************************************** // * // * Convenience // * // ***************************************************** /** * Puts up the selection view. * * @param range * @param text * @param handleBounds * @return */ protected void handleSelection(String range, String text, String handleBounds) { try { JSONObject selectionBoundsObject = new JSONObject(handleBounds); float scale = getDensityIndependentValue(getScale(), mContext); Rect handleRect = new Rect(); handleRect.left = (int) (getDensityDependentValue(selectionBoundsObject.getInt("left"), getContext()) * scale); handleRect.top = (int) (getDensityDependentValue(selectionBoundsObject.getInt("top"), getContext()) * scale); handleRect.right = (int) (getDensityDependentValue(selectionBoundsObject.getInt("right"), getContext()) * scale); handleRect.bottom = (int) (getDensityDependentValue(selectionBoundsObject.getInt("bottom"), getContext()) * scale); mSelectionBounds = handleRect; mSelectedRange = range; mSelectedText = text; if (!isInSelectionMode()) { startSelectionMode(); } drawSelectionHandles(); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * Calculates the context menu display rect * * @param menuBounds * @return The display Rect */ protected Rect getContextMenuBounds(String menuBounds) { try { JSONObject menuBoundsObject = new JSONObject(menuBounds); float scale = getDensityIndependentValue(getScale(), mContext); Rect displayRect = new Rect(); displayRect.left = (int) (getDensityDependentValue(menuBoundsObject.getInt("left"), getContext()) * scale); displayRect.top = (int) (getDensityDependentValue(menuBoundsObject.getInt("top") - 25, getContext()) * scale); displayRect.right = (int) (getDensityDependentValue(menuBoundsObject.getInt("right"), getContext()) * scale); displayRect.bottom = (int) (getDensityDependentValue(menuBoundsObject.getInt("bottom") + 25, getContext()) * scale); return displayRect; } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } // ***************************************************** // * // * Density Conversion // * // ***************************************************** /** * Returns the density dependent value of the given float * * @param val * @param ctx * @return */ public float getDensityDependentValue(float val, Context ctx) { // Get display from context Display display = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); // Calculate min bound based on metrics DisplayMetrics metrics = new DisplayMetrics(); display.getMetrics(metrics); return val * (metrics.densityDpi / 160f); // return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, val, // metrics); } /** * Returns the density independent value of the given float * * @param val * @param ctx * @return */ public float getDensityIndependentValue(float val, Context ctx) { // Get display from context Display display = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); // Calculate min bound based on metrics DisplayMetrics metrics = new DisplayMetrics(); display.getMetrics(metrics); return val / (metrics.densityDpi / 160f); // return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, val, // metrics); } // TODO ? public String submitCondition() { StringBuilder sb = new StringBuilder(); Set<String> set = lineView.getData(); for (String selectStr : set) { String[] array = selectStr.split(SELECT_SPLIT); sb.append(array[0]).append("\n"); } Toast.makeText(mContext, sb.toString(), Toast.LENGTH_LONG).show(); return sb.toString(); } /** * ?? */ public List<String> getSelection() { List<String> list = new ArrayList<String>(); Set<String> set = lineView.getData(); for (String selectStr : set) { String[] array = selectStr.split(SELECT_SPLIT); list.add(array[0]); } return list; } @Override public void tsjiSelectionClientRects(String rectStr, String selectContext) { // ???bottom // top+ ??bottom try { float scale = getDensityIndependentValue(getScale(), mContext); JSONArray jsonArray = new JSONArray(rectStr); int length = jsonArray.length(); this.selectContext = selectContext; // TODO ,?new rectList = new ArrayList<Rect>(); for (int i = 0; i < length; i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); Rect handleRect = new Rect(); handleRect.left = (int) (getDensityDependentValue(jsonObject.getInt("left"), getContext()) * scale); handleRect.top = (int) (getDensityDependentValue(jsonObject.getInt("top"), getContext()) * scale); handleRect.right = (int) (getDensityDependentValue(jsonObject.getInt("right"), getContext()) * scale); handleRect.bottom = (int) (getDensityDependentValue(jsonObject.getInt("bottom"), getContext()) * scale); // TODO // handleRect.bottom = 92; rectList.add(handleRect); } } catch (JSONException e) { // e.printStackTrace(); } } /** * ? */ public void closeLineMode() { this.isLineMode = false; } /** * */ public void clearLine() { lineView.removeAll(); myLayout.removeAllViews(); myLayout.addView(lineView); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { if (isLineMode) { scrollY = t; myLayout.scrollTo(l, t); } super.onScrollChanged(l, t, oldl, oldt); } /** * */ public interface OnTextSelectListener { /** * * * @param text */ void select(String text); /** * ? * * @param text ? */ void cancle(String text); } /** * ? */ private OnTextSelectListener onTextSelectListener; public void setOnTextSelectListener(OnTextSelectListener onTextSelectListener) { this.onTextSelectListener = onTextSelectListener; } }