How to override default text selection of android webview os 4.1+? - android
Before posting this question I've searched a lot but could not find any clear answers on this issue.
I have to override default text selection of android webview & show my custom text selection dialog options. I have tried this sample code project.
This sample project works on following devices & emulator :
Acer Iconia a500 tablet : 10 inch : Android OS - 3.0
Acer Iconia a500 tablet : 10 inch : Android OS - 3.2
Samsung Galaxy Tab : 10 inch : Android OS - 4.0
Samsung Galaxy Tab : 7 inch : Android OS - 4.0
Emulator : Skin-WVGA800 : Android OS - 4.1.2
Not working on following devices :
Samsung Galaxy Tab : 10 inch : Android OS - 4.1.2
Samsung Galaxy Tab : 7 inch : Android OS - 4.1.2
On android os version 4.1 & 4.1+ instead of showing my custom text selection option dialog, it shows android system's default action bar for text selection.
I have searched a lot on this, many suggested to use onLongClick() method of the interface
I have already asked a question on this forum please see this link, with answers to this questions I am able to clone onLongClick() event but I can't stop default text selection action bar.
For this scenario I have few questions.
1.Why onLongClick() method stops working for device running on android os version 4.1+ ?
2.How to stop default text selection action bar on long pressing on the text from webview ?
This is my custom webview class.
package com.epubreader.ebook;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Context;
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.util.Log;
import android.view.ContextMenu;
import android.view.Display;
import android.view.GestureDetector;
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.WebView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.epubreader.R;
import com.epubreader.drag.DragController;
import com.epubreader.drag.DragLayer;
import com.epubreader.drag.DragListener;
import com.epubreader.drag.DragSource;
import com.epubreader.drag.MyAbsoluteLayout;
import com.epubreader.menu.menuAnimationHelper;
import com.epubreader.textselection.WebTextSelectionJSInterface;
import com.epubreader.textselectionoverlay.ActionItem;
import com.epubreader.textselectionoverlay.QuickAction;
import com.epubreader.textselectionoverlay.QuickAction.OnDismissListener;
public class CustomWebView extends WebView implements WebTextSelectionJSInterface,
OnTouchListener , OnLongClickListener, OnDismissListener, DragListener{
/** The logging tag. */
private static final String TAG = "CustomWebView";
/** Context. */
protected Context ctx;
/** The context menu. */
private QuickAction mContextMenu;
/** The drag layer for selection. */
private DragLayer mSelectionDragLayer;
/** The drag controller for selection. */
private DragController mDragController;
/** The start selection handle. */
private ImageView mStartSelectionHandle;
/** the end selection handle. */
private ImageView mEndSelectionHandle;
/** The selection bounds. */
private Rect mSelectionBounds = null;
/** The previously selected region. */
protected Region lastSelectedRegion = null;
/** The selected range. */
protected String selectedRange = "";
/** The selected text. */
protected String selectedText = "";
/** Javascript interface for catching text selection. */
/** Selection mode flag. */
protected boolean inSelectionMode = false;
/** Flag to stop from showing context menu twice. */
protected boolean contextMenuVisible = false;
/** The current content width. */
protected int contentWidth = 0;
/** Identifier for the selection start handle. */
private final int SELECTION_START_HANDLE = 0;
/** Identifier for the selection end handle. */
private final int SELECTION_END_HANDLE = 1;
/** Last touched selection handle. */
private int mLastTouchedSelectionHandle = -1;
/** Variables for Left & Right Menu ***/
private View menuView;
private LinearLayout toiLay;
private menuAnimationHelper _menuAnimationHelper;
private TocTranslateAnimation _tocTranslateAnimation;
private CustomWebView _customWebView;
public CustomWebView(Context context) {
super(context);
this.ctx = context;
this.setup(context);
}
public CustomWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.ctx = context;
this.setup(context);
}
public CustomWebView(Context context, AttributeSet attrs) {
super(context, attrs);
this.ctx = context;
this.setup(context);
}
//*****************************************************
//*
//* Touch Listeners
//*
//*****************************************************
private boolean mScrolling = false;
private float mScrollDiffY = 0;
private float mLastTouchY = 0;
private float mScrollDiffX = 0;
private float mLastTouchX = 0;
#Override
public boolean onTouch(View v, MotionEvent event) {
float xPoint = getDensityIndependentValue(event.getX(), ctx) / getDensityIndependentValue(this.getScale(), ctx);
float yPoint = getDensityIndependentValue(event.getY(), ctx) / getDensityIndependentValue(this.getScale(), ctx);
// TODO: Need to update this to use this.getScale() as a factor.
//Log.d(TAG, "onTouch " + xPoint + " , " + yPoint);
closeMenu();
if(event.getAction() == MotionEvent.ACTION_DOWN){
final String startTouchUrl = String.format("javascript:android.selection.startTouch(%f, %f);",
xPoint, yPoint);
mLastTouchX = xPoint;
mLastTouchY = yPoint;
((Activity)this.ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl(startTouchUrl);
}
});
// This two line clones the onLongClick()
longClickHandler.removeCallbacks(longClickRunnable);
longClickHandler.postDelayed(longClickRunnable,300);
}
else if(event.getAction() == MotionEvent.ACTION_UP){
// Check for scrolling flag
if(!mScrolling){
this.endSelectionMode();
}
// This line clones the onLongClick()
longClickHandler.removeCallbacks(longClickRunnable);
mScrollDiffX = 0;
mScrollDiffY = 0;
mScrolling = false;
}else if(event.getAction() == MotionEvent.ACTION_MOVE){
mScrollDiffX += (xPoint - mLastTouchX);
mScrollDiffY += (yPoint - mLastTouchY);
mLastTouchX = xPoint;
mLastTouchY = yPoint;
// Only account for legitimate movement.
if(Math.abs(mScrollDiffX) > 10 || Math.abs(mScrollDiffY) > 10){
mScrolling = true;
}
// This line clones the onLongClick()
longClickHandler.removeCallbacks(longClickRunnable);
}
// If this is in selection mode, then nothing else should handle this touch
return false;
}
/**
* Pass References of Left & Right Menu
*/
public void initMenu(LinearLayout _toiLay,View _menuView,menuAnimationHelper menuAnimationHelper,
TocTranslateAnimation tocTranslateAnimation){
toiLay = _toiLay;
menuView = _menuView;
_menuAnimationHelper = menuAnimationHelper;
_tocTranslateAnimation = tocTranslateAnimation;
}
private void closeMenu(){
if(_menuAnimationHelper != null && _menuAnimationHelper.isMenuOpenBool){
_menuAnimationHelper.close(menuView);
_menuAnimationHelper.isMenuOpenBool = false;
}
if(_tocTranslateAnimation != null && _tocTranslateAnimation.isTocListOpenBool){
_tocTranslateAnimation.close(toiLay);
_tocTranslateAnimation.isTocListOpenBool = false;
}
}
public void removeOverlay(){
Log.d("JsHandler", "in java removeOverlay" + mScrolling);
this.endSelectionMode();
mScrollDiffX = 0;
mScrollDiffY = 0;
mScrolling = false;
}
#Override
public boolean onLongClick(View v){
Log.d(TAG, "from webView onLongClick ");
mScrolling = true;
((Activity)this.ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl("javascript:android.selection.longTouch()");
}
});
Toast.makeText(ctx, "Long click is clicked ", Toast.LENGTH_LONG).show();
// Don't let the webview handle it
return true;
}
//*****************************************************
//*
//* Setup
//*
//*****************************************************
ContextMenu.ContextMenuInfo contextMenuInfo;
/**
* Setups up the web view.
* #param context
*/
protected void setup(Context context){
// On Touch Listener
this.setOnTouchListener(this);
this.setClickable(false);
this.setLongClickable(true);
this.setOnLongClickListener(this);
contextMenuInfo = this.getContextMenuInfo();
// Webview setup
this.getSettings().setJavaScriptEnabled(true);
this.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
// Create the selection handles
createSelectionLayer(context);
// Set to the empty region
Region region = new Region();
region.setEmpty();
_customWebView = this;
this.lastSelectedRegion = region;
}
/**
* To clone OnLongClick Listener because its not responding for version 4.1
*/
public Runnable longClickRunnable = new Runnable() {
public void run() {
longClickHandler.sendEmptyMessage(0);
}
};
public Handler longClickHandler = new Handler(){
public void handleMessage(Message m){
_customWebView.loadUrl("javascript:android.selection.longTouch();");
mScrolling = true;
}
};
public WebTextSelectionJSInterface getTextSelectionJsInterface(){
return this;
}
//*****************************************************
//*
//* Selection Layer Handling
//*
//*****************************************************
/**
* Creates the selection layer.
*
* #param context
*/
protected void createSelectionLayer(Context context){
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.mSelectionDragLayer = (DragLayer) inflater.inflate(R.layout.selection_drag_layer, null);
// Make sure it's filling parent
this.mDragController = new DragController(context);
this.mDragController.setDragListener(this);
this.mDragController.addDropTarget(mSelectionDragLayer);
this.mSelectionDragLayer.setDragController(mDragController);
this.mStartSelectionHandle = (ImageView) this.mSelectionDragLayer.findViewById(R.id.startHandle);
this.mStartSelectionHandle.setTag(new Integer(SELECTION_START_HANDLE));
this.mEndSelectionHandle = (ImageView) this.mSelectionDragLayer.findViewById(R.id.endHandle);
this.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;
}
};
this.mStartSelectionHandle.setOnTouchListener(handleTouchListener);
this.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(), ctx));
// Update Layout Params
ViewGroup.LayoutParams layerParams = mSelectionDragLayer.getLayoutParams();
layerParams.height = contentHeight;
layerParams.width = contentWidth;
mSelectionDragLayer.setLayoutParams(layerParams);
}
};
/**
* Starts selection mode.
*
* #param selectionBounds
*/
public void startSelectionMode(){
this.startSelectionModeHandler.sendEmptyMessage(0);
}
// Ends selection mode on the UI thread
private Handler endSelectionModeHandler = new Handler(){
public void handleMessage(Message m){
//Log.d("TableContentsWithDisplay", "in endSelectionModeHandler");
removeView(mSelectionDragLayer);
if(getParent() != null && mContextMenu != null && contextMenuVisible){
// 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;
try {
((Activity)ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl("javascript: android.selection.clearSelection();");
}
});
} catch (Exception e) {
// TODO: handle exception
}
}
};
/**
* Ends selection mode.
*/
public void endSelectionMode(){
this.endSelectionModeHandler.sendEmptyMessage(0);
}
/**
* Calls the handler for drawing the selection handles.
*/
private void drawSelectionHandles(){
this.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 = (com.epubreader.drag.MyAbsoluteLayout.LayoutParams) mStartSelectionHandle.getLayoutParams();
startParams.x = (int) (mSelectionBounds.left - mStartSelectionHandle.getDrawable().getIntrinsicWidth());
startParams.y = (int) (mSelectionBounds.top - mStartSelectionHandle.getDrawable().getIntrinsicHeight());
// 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 = (com.epubreader.drag.MyAbsoluteLayout.LayoutParams) mEndSelectionHandle.getLayoutParams();
endParams.x = (int) mSelectionBounds.right;
endParams.y = (int) mSelectionBounds.bottom;
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 this.mSelectionDragLayer.getParent() != null;
}
//*****************************************************
//*
//* 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.
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#SuppressWarnings("deprecation")
public void onDragEnd() {
// TODO Auto-generated method stub
MyAbsoluteLayout.LayoutParams startHandleParams = (MyAbsoluteLayout.LayoutParams) this.mStartSelectionHandle.getLayoutParams();
MyAbsoluteLayout.LayoutParams endHandleParams = (MyAbsoluteLayout.LayoutParams) this.mEndSelectionHandle.getLayoutParams();
float scale = getDensityIndependentValue(this.getScale(), ctx);
float startX = startHandleParams.x - this.getScrollX();
float startY = startHandleParams.y - this.getScrollY();
float endX = endHandleParams.x - this.getScrollX();
float endY = endHandleParams.y - this.getScrollY();
startX = getDensityIndependentValue(startX, ctx) / scale;
startY = getDensityIndependentValue(startY, ctx) / scale;
endX = getDensityIndependentValue(endX, ctx) / scale;
endY = getDensityIndependentValue(endY, ctx) / scale;
if(mLastTouchedSelectionHandle == SELECTION_START_HANDLE && startX > 0 && startY > 0){
final String saveStartString = String.format("javascript: android.selection.setStartPos(%f, %f);", startX, startY);
((Activity)ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl(saveStartString);
}
});
}
if(mLastTouchedSelectionHandle == SELECTION_END_HANDLE && endX > 0 && endY > 0){
final String saveEndString = String.format("javascript: android.selection.setEndPos(%f, %f);", endX, endY);
((Activity)ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl(saveEndString);
}
});
}
}
//*****************************************************
//*
//* Context Menu Creation
//*
//*****************************************************
/**
* Shows the context menu using the given region as an anchor point.
* #param region
*/
private void showContextMenu(Rect displayRect){
// Don't show this twice
if(this.contextMenuVisible){
return;
}
// Don't use empty rect
//if(displayRect.isEmpty()){
if(displayRect.right <= displayRect.left){
return;
}
//Copy action item
ActionItem buttonOne = new ActionItem();
buttonOne.setTitle("HighLight");
buttonOne.setActionId(1);
//buttonOne.setIcon(getResources().getDrawable(R.drawable.menu_search));
//Highlight action item
ActionItem buttonTwo = new ActionItem();
buttonTwo.setTitle("Note");
buttonTwo.setActionId(2);
//buttonTwo.setIcon(getResources().getDrawable(R.drawable.menu_info));
ActionItem buttonThree = new ActionItem();
buttonThree.setTitle("Help");
buttonThree.setActionId(3);
//buttonThree.setIcon(getResources().getDrawable(R.drawable.menu_eraser));
// The action menu
mContextMenu = new QuickAction(this.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) {
if (actionId == 1) {
callHighLight();
}
else if (actionId == 2) {
callNote();
}
else if (actionId == 3) {
// Do Button 3 stuff
Log.i(TAG, "Hit Button 3");
}
contextMenuVisible = false;
}
});
this.contextMenuVisible = true;
mContextMenu.show(this, displayRect);
}
private void callHighLight(){
((Activity)this.ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl("javascript:init_txt_selection_event()");
loadUrl("javascript:highlightme_("+0+")");
}
});
}
private void callNote(){
((Activity)this.ctx).runOnUiThread(new Runnable() {
#Override
public void run() {
loadUrl("javascript:init_txt_selection_event()");
loadUrl("javascript:fnGetUserAddedNote('1')");
}
});
}
//*****************************************************
//*
//* OnDismiss Listener
//*
//*****************************************************
/**
* Clears the selection when the context menu is dismissed.
*/
public void onDismiss(){
//clearSelection();
this.contextMenuVisible = false;
}
//*****************************************************
//*
//* Text Selection Javascript Interface Listener
//*
//*****************************************************
/**
* The user has started dragging the selection handles.
*/
public void tsjiStartSelectionMode(){
this.startSelectionMode();
}
/**
* The user has stopped dragging the selection handles.
*/
public void tsjiEndSelectionMode(){
this.endSelectionMode();
}
/**
* The selection has changed
* #param range
* #param text
* #param handleBounds
* #param menuBounds
* #param showHighlight
* #param showUnHighlight
*/#SuppressWarnings("deprecation")
public void tsjiSelectionChanged(String range, String text, String handleBounds, String menuBounds){
try {
//Log.d(TAG, "tsjiSelectionChanged :- handleBounds " + handleBounds);
JSONObject selectionBoundsObject = new JSONObject(handleBounds);
float scale = getDensityIndependentValue(this.getScale(), ctx);
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);
this.mSelectionBounds = handleRect;
this.selectedRange = range;
this.selectedText = text;
JSONObject menuBoundsObject = new JSONObject(menuBounds);
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);
if(!this.isInSelectionMode()){
this.startSelectionMode();
}
// This will send the menu rect
this.showContextMenu(displayRect);
drawSelectionHandles();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Receives the content width for the page.
*/
public void tsjiSetContentWidth(float contentWidth){
this.contentWidth = (int) this.getDensityDependentValue(contentWidth, ctx);
}
//*****************************************************
//*
//* 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);
}
/**
* 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);
}
}
Thanks In Advance!
I realize this is an old question, but there is not an accepted answer, and none of the provided answers have many votes.
I have achieved what you are trying to accomplish by creating a custom action mode callback. This allows the creation of a custom contextual action bar (CAB) when (in the case of a WebView) a user long-clicks the view. NOTE: This only works in 4.0+ and one piece only works in 4.4.
Create a new Android XML file containing the layout for your custom menu. Here is mine (context_menu.xml) as an example:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="#+id/copy"
android:icon="#drawable/ic_action_copy"
android:showAsAction="always" <!-- Forces this button to be shown -->
android:title="#string/copy">
</item>
<item
android:id="#+id/button2"
android:icon="#drawable/menu_button2icon"
android:showAsAction="ifRoom" <!-- Show if there is room on the screen-->
android:title="#string/button2">
</item>
<!-- Add as many more items as you want.
Note that if you use "ifRoom", an overflow menu will populate
to hold the action items that did not fit in the action bar. -->
</menu>
Now that the menu is defined, create a callback for it:
public class CustomWebView extends WebView {
private ActionMode.Callback mActionModeCallback;
private class CustomActionModeCallback implements ActionMode.Callback {
// Called when the action mode is created; startActionMode() was called
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.context_menu, menu);
return true;
}
// Called each time the action mode is shown.
// Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
#Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Note: This is called every time the selection handlebars move.
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
#Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.copy:
// Do some stuff for this button
mode.finish(); // Action picked, so close the CAB
return true;
case R.id.button2:
// Do some stuff for this button
mode.finish();
return true;
// Create a case for every item
...
default:
mode.finish();
return false;
}
}
// Called when the user exits the action mode
#Override
public void onDestroyActionMode(ActionMode mode) {
// This will clear the selection highlight and handlebars.
// However, it only works in 4.4; I am still investigating
// how to reliably clear the selection in 4.3 and earlier
clearFocus();
}
}
Once those pieces are in place, override the startActionMode method so that the callback will actually occur on a long-click:
public class CustomWebView extends WebView {
#Override
public ActionMode startActionMode(Callback callback) {
ViewParent parent = getParent();
if (parent == null) {
return null;
}
mActionModeCallback = new CustomActionModeCallback();
return parent.startActionModeForChild(this, mActionModeCallback);
}
}
Now you have a custom menu that will be appear and populate when a user long-clicks your WebView. For reference, I found this information from an Android Developer tutorial and modified the suggestion from this answer.
One final note: This technique overrides the native copy/paste menu for selected text. If you want to maintain that functionality, you will need to implement it yourself. (That is why my first button is 'copy'.) If you need that, refer to this Google tutorial for more information and the proper way to implement it.
Use could the setOnTouchListener() for long/short long press. and return true so that nothing happens thereby overriding the default text selection feature.
I have been able to resolve this. I was also facing this issue and could not find any solution on the web.
So, if you set up a LongClick listener, the Webview would stop showing selection at all. After delving deep into the Webview code, I found that it was calling WebView's method startRunMode and passing an instance of SelectActionCallbackMode class.
I simply extended the Webview class and overrided the startRunMode method like this:
public ActionMode startActionMode(ActionMode.Callback callback)
{
actionModeCallback = new CustomizedSelectActionModeCallback();
return super.startActionMode(actionModeCallback);
}
This forced the Webview to display my Callback instead of displaying Webview's default one. This ensured that selection worked as smoothly as before and my CAB was displayed each time selection was made. Only caveat was that I had to write code to dismiss the CAB myself.
Tested on 4.1, 4.2 and 4.3 devices.
Hope this helps.
I could suggest a workaround for this.
You could use setOnTouchListener and then implement the long press yourself.
onTouch --
case MotionEvent.ACTION_DOWN:
mHanlder.removeCallbacks(startActionBar);
mHandler.postDelayed(startActionBar,1000);
This way you could achieve the same aciton.
some reason the KeyEvent down & up doesn't work in API LEVEL 16+ (Android 4.1+ JELLY_BEAN). It doesn't fire the WebView's loadUrl. So I had to replace the dispatchKeyEvent with dispatchTouchEvent. Here's the code:
...
MotionEvent touchDown = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, touchX, touchY, 0);
webView.dispatchTouchEvent(touchDown);
touchDown.recycle();
MotionEvent touchUp = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, touchX, touchY, 0);
webView.dispatchTouchEvent(touchUp);
touchUp.recycle();
String url = mUrl;
...
try it..
For disabling your text selection from webview try this,
webView.setWebChromeClient(new WebChromeClient(){
[.... other overrides....]
// #Override
// https://bugzilla.wikimedia.org/show_bug.cgi?id=31484
// If you DO NOT want to start selection by long click,
// the remove this function
// (All this is undocumented stuff...)
public void onSelectionStart(WebView view) {
// By default we cancel the selection again, thus disabling
// text selection unless the chrome client supports it.
// view.notifySelectDialogDismissed();
}
});
You might also try overriding View.performLongClick(), which is responsible for calling View.onLongPress(). You could also try going up to the parent View's long press events. Or all the way up to the Window.Callback for your activity (via Activity.getWindow().get/setCallback()).
I'm wondering whether there are other ways for the standard selection context menu to appear besides the long-press event, for example maybe with a trackball or hardware keyboard.
in case someone is trying to simply remove the default text selection, I had the same issue on a Samsung Galaxy Tab on Android 4.1.2 and ended up writing my own WebView:
public class CustomWebView extends WebView {
public CustomWebView(Context context) {
super(context);
this.setUp(context);
}
public CustomWebView(Context context, AttributeSet attrs) {
super(context, attrs);
this.setUp(context);
}
public CustomWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.setUp(context);
}
private void setUp(Context context) {
this.setOnLongClickListener(new OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
return true;
}
});
this.setLongClickable(false);
}
#Override
public boolean performLongClick() {
return true;
}
}
and referring it in my xml:
<com...CustomWebView
android:id="#+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
Use setOnTouchListener() implement long press
Related
Vertical ViewPager and Android Pie Inconsistent Behavior with Swipe Gesture
My problem is closely related to two other questions that haven't been answered yet. ViewPager not responding to touch in layout area created dynamically in Fragment https://stackoverflow.com/questions/53469581/problem-with-vertical-viewpager-like-inshorts My Vertical ViewPager works wonderfully and consistently within any device I have tested and with any OS 5 - 8. I recently upgraded a pixel 2XL with Android Pie and now my Vertical ViewPager appears to be unresponsive, then works, then it acts like it loses focus, then works. Drag a page and it moves and snaps back to original position. Or just bounces back. Again, similar to the other two questions linked above. Prior to Android 9, vertical scrolling and paging is perfect. I've tried using reflection with a little success. It will swipe better and doesn't seem to lose focus as much. But if I try swiping with my other hand it stops, or if I change my placement of where I am swiping it will stop. This is very perplexing. I have added all the code required to replicate this issue on a device running Android 9. The Activity public class FullscreenActivity extends AppCompatActivity { VerticalViewPager verticalViewPager; FragmentStatePagerExample fragmentStatePagerExample; int pagerPadding; /** * Whether or not the system UI should be auto-hidden after * {#link #AUTO_HIDE_DELAY_MILLIS} milliseconds. */ private static final boolean AUTO_HIDE = true; /** * If {#link #AUTO_HIDE} is set, the number of milliseconds to wait after * user interaction before hiding the system UI. */ private static final int AUTO_HIDE_DELAY_MILLIS = 3000; /** * Some older devices needs a small delay between UI widget updates * and a change of the status and navigation bar. */ private static final int UI_ANIMATION_DELAY = 300; private final Handler mHideHandler = new Handler(); private FrameLayout mContentView; private final Runnable mHidePart2Runnable = new Runnable() { #SuppressLint("InlinedApi") #Override public void run() { // Delayed removal of status and navigation bar // Note that some of these constants are new as of API 16 (Jelly Bean) // and API 19 (KitKat). It is safe to use them, as they are inlined // at compile-time and do nothing on earlier devices. verticalViewPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); } }; private View mControlsView; private final Runnable mShowPart2Runnable = new Runnable() { #Override public void run() { // Delayed display of UI elements ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.show(); } mControlsView.setVisibility(View.VISIBLE); } }; private boolean mVisible; private final Runnable mHideRunnable = new Runnable() { #Override public void run() { hide(); } }; /** * Touch listener to use for in-layout UI controls to delay hiding the * system UI. This is to prevent the jarring behavior of controls going away * while interacting with activity UI. */ private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() { #Override public boolean onTouch(View view, MotionEvent motionEvent) { if (AUTO_HIDE) { delayedHide(AUTO_HIDE_DELAY_MILLIS); } return false; } }; #Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fullscreen); pagerPadding = getScreenDimension(this); mVisible = true; mControlsView = findViewById(R.id.fullscreen_content_controls); verticalViewPager = findViewById(R.id.main_viewpager); verticalViewPager.setPadding(0,0,0,pagerPadding); verticalViewPager.setClipToPadding(false); fragmentStatePagerExample = new FragmentStatePagerExample(getSupportFragmentManager()); verticalViewPager.setAdapter(fragmentStatePagerExample); verticalViewPager.setCurrentItem(0); // Set up the user interaction to manually show or hide the system UI. verticalViewPager.setOnClickListener(new View.OnClickListener() { #Override public void onClick(View view) { toggle(); } }); // Upon interacting with UI controls, delay any scheduled hide() // operations to prevent the jarring behavior of controls going away // while interacting with the UI. findViewById(R.id.dummy_button).setOnTouchListener(mDelayHideTouchListener); } #Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Trigger the initial hide() shortly after the activity has been // created, to briefly hint to the user that UI controls // are available. delayedHide(100); } private void toggle() { if (mVisible) { hide(); } else { show(); } } private void hide() { // Hide UI first ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.hide(); } mControlsView.setVisibility(View.GONE); mVisible = false; // Schedule a runnable to remove the status and navigation bar after a delay mHideHandler.removeCallbacks(mShowPart2Runnable); mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY); } #SuppressLint("InlinedApi") private void show() { // Show the system bar mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); mVisible = true; // Schedule a runnable to display UI elements after a delay mHideHandler.removeCallbacks(mHidePart2Runnable); mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); } /** * Schedules a call to hide() in delay milliseconds, canceling any * previously scheduled calls. */ private void delayedHide(int delayMillis) { mHideHandler.removeCallbacks(mHideRunnable); mHideHandler.postDelayed(mHideRunnable, delayMillis); } private static int getScreenDimension(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); DisplayMetrics metrics = new DisplayMetrics(); display.getMetrics(metrics); int width = metrics.widthPixels; int height = metrics.heightPixels; return (int)Math.round(height * .2); } } The Fragment public class ImageFragment extends Fragment{ ImageView imageView; String imageUrl = ""; #Override public void onCreate(#Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } #Nullable #Override public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); Bundle bundle = getArguments(); imageUrl = bundle.getString("url"); return inflater.inflate(R.layout.fragment_image, container,false); } #Override public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); imageView = (ImageView)view.findViewById(R.id.iv_imagefragment); Glide.with(getActivity()).load(imageUrl).into(imageView); } public static Fragment getInstance(int position, String url){ Bundle bundle = new Bundle(); bundle.putString("url",url); ImageFragment fragment = new ImageFragment(); fragment.setArguments(bundle); return fragment; } } The ViewPager public class VerticalViewPager extends ViewPager { public VerticalViewPager(Context context) { super(context); init(); } public VerticalViewPager(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { // The majority of the magic happens here setPageTransformer(true, new VerticalPageTransformer()); setOffscreenPageLimit(2); // The easiest way to get rid of the overscroll drawing that happens on the left and right setOverScrollMode(OVER_SCROLL_NEVER); } private class VerticalPageTransformer implements ViewPager.PageTransformer { #Override public void transformPage(View view, float position) { if (position < -1) { // [-Infinity,-1) // This page is way off-screen to the left. view.setAlpha(0); } else if (position <= 1) { // [-1,1] view.setAlpha(1); // Counteract the default slide transition view.setTranslationX(view.getWidth() * -position); //set Y position to swipe in from top float yPosition = position * view.getHeight(); view.setTranslationY(yPosition); } else { // (1,+Infinity] // This page is way off-screen to the right. view.setAlpha(0); } } } /** * Swaps the X and Y coordinates of your touch event. */ private MotionEvent swapXY(MotionEvent ev) { float width = getWidth(); float height = getHeight(); float newX = (ev.getY() / height) * width; float newY = (ev.getX() / width) * height; ev.setLocation(newX, newY); return ev; } #Override public boolean onInterceptTouchEvent(MotionEvent ev){ boolean intercepted = super.onInterceptTouchEvent(swapXY(ev)); swapXY(ev); // return touch coordinates to original reference frame for any child views return intercepted; } #Override public boolean onTouchEvent(MotionEvent ev) { return super.onTouchEvent(swapXY(ev)); } } The ViewPager Adapter public class FragmentStatePagerExample extends FragmentStatePagerAdapter { String url = ""; public FragmentStatePagerExample(FragmentManager fm) { super(fm); } #Override public Fragment getItem(int position) { switch (position){ case 0: url = "https://images.unsplash.com/photo-1532977692289-827d858a170b?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=29b1d5377ad9db8de64b1b73d21812c7&auto=format&fit=crop&w=1474&q=80"; return ImageFragment.getInstance(position,url); case 1: url = "https://images.unsplash.com/photo-1533029516911-0458c644baea?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=0f618e036e338f48ef919b8fb86c5ba1&auto=format&fit=crop&w=701&q=80"; return ImageFragment.getInstance(position,url); case 2: url = "https://images.unsplash.com/photo-1532989622000-d4f013a215e1?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=1a69643c04176376315714b9b2897de5&auto=format&fit=crop&w=677&q=80"; return ImageFragment.getInstance(position,url); default: url = "https://images.unsplash.com/photo-1532983819500-85d633c73b7a?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=1f0b228b67f03064241534a6c65d9497&auto=format&fit=crop&w=1050&q=80"; return ImageFragment.getInstance(position,url); } } #Override public int getCount() { return 4; } } Activity XML <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#0099cc" tools:context=".FullscreenActivity"> <!-- This FrameLayout insets its children based on system windows using android:fitsSystemWindows. --> <com.david.verticalviewpagerexample.VerticalViewPager android:id="#+id/main_viewpager" android:layout_width="match_parent" android:layout_height="match_parent"/> <LinearLayout android:id="#+id/fullscreen_content_controls" style="?metaButtonBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:background="#color/black_overlay" android:orientation="horizontal" tools:ignore="UselessParent"> <Button android:id="#+id/dummy_button" style="?metaButtonBarButtonStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="#string/dummy_button" /> </LinearLayout> </FrameLayout> Fragment XML <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="#+id/iv_imagefragment" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop"/> </LinearLayout> Update : 1 https://github.com/youngkaaa/YViewPagerDemo There is another library and that works really smooth on Android Pie, but it has few soft crashes. Also, it crashes on API 19. Update : 2 Google has recently released ViewPager2 with androidx support library https://developer.android.com/jetpack/androidx/releases/viewpager2, that supports vertical viewpager. However, it is still in alpha version and it has many known issues.
After spending hell amount of time on SO, trying many GitHub libraries and waiting for someone to respond on the bounty, then I came up with below solutions and hope that it helps whoever needs it. At first, this question got my attention and I think most of the answers over there are helpful, hence I upvoted them. The main answer which helped me are link-1 and link-2. Even though I have to make a few minor changes to ignore horizontal swipe fling events. Please do try this code and provide your feedback for any further improvements, thanks in advance. Happy Coding :) import android.content.Context; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class VerticalViewPager extends ViewPager { float x = 0; float mStartDragX = 0; private static final float SWIPE_X_MIN_THRESHOLD = 50; // Decide this magical nuber as per your requirement public VerticalViewPager(Context context) { super(context); init(); } public VerticalViewPager(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { // The majority of the magic happens here setPageTransformer(true, new VerticalPageTransformer()); // The easiest way to get rid of the overscroll drawing that happens on the left and right setOverScrollMode(OVER_SCROLL_NEVER); } #Override public boolean onTouchEvent(MotionEvent event) { if (getAdapter() != null) { if (getCurrentItem() >= 0 || getCurrentItem() < getAdapter().getCount()) { swapXY(event); final int action = event.getAction(); switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: mStartDragX = event.getX(); if (x < mStartDragX && (mStartDragX - x > SWIPE_X_MIN_THRESHOLD) && getCurrentItem() > 0) { Log.i("VerticalViewPager", "down " + x + " : " + mStartDragX + " : " + (mStartDragX - x)); setCurrentItem(getCurrentItem() - 1, true); return true; } else if (x > mStartDragX && (x - mStartDragX > SWIPE_X_MIN_THRESHOLD) && getCurrentItem() < getAdapter().getCount() - 1) { Log.i("VerticalViewPager", "up " + x + " : " + mStartDragX + " : " + (x - mStartDragX)); mStartDragX = 0; setCurrentItem(getCurrentItem() + 1, true); return true; } break; } } else { mStartDragX = 0; } swapXY(event); return super.onTouchEvent(swapXY(event)); } return false; } #Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = super.onInterceptTouchEvent(swapXY(event)); switch (event.getAction() & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: x = event.getX(); break; } swapXY(event); // return touch coordinates to original reference frame for any child views return intercepted; } /** * Swaps the X and Y coordinates of your touch event. */ private MotionEvent swapXY(MotionEvent ev) { float width = getWidth(); float height = getHeight(); float newX = (ev.getY() / height) * width; float newY = (ev.getX() / width) * height; ev.setLocation(newX, newY); return ev; } private class VerticalPageTransformer implements PageTransformer { #Override public void transformPage(View view, float position) { if (position < -1) { // [-Infinity,-1) // This page is way off-screen to the left. view.setAlpha(0); } else if (position <= 1) { // [-1,1] view.setAlpha(1); // Counteract the default slide transition view.setTranslationX(view.getWidth() * -position); //set Y position to swipe in from top float yPosition = position * view.getHeight(); view.setTranslationY(yPosition); } else { // (1,+Infinity] // This page is way off-screen to the right. view.setAlpha(0); } } } }
You can try this lib: VerticalViewPager, this works fine on my project. But this lib is copied from v19, so some methods will not exist, you can implement yourself.
The problem seems to be in the way #swapXY exchanges the x and y values. I believe that the VelocityTracker used in ViewPager uses #getAxisValue and not #getX/#getY, and the result is not swapped. But there doesn't seem to be a way to set the axis values nor to subclass MotionEvent so I didn't find a way to stick with the simple #swapXY solution. I forked ViewPager and use VelocityTracker.getYVelocity when I detect the unswapped condition. diff --git project/src/main/java/org/gc/project/util/ShopVerticalViewPager.java project/src/main/java/org/gc/project/util/ShopVerticalViewPager.java index e5560a0..f23f9f7 100644 --- project/src/main/java/org/gc/project/util/MyVerticalViewPager.java +++ project/src/main/java/org/gc/project/util/MyVerticalViewPager.java ## -179,4 +179,8 ## public class ShopVerticalViewPager extends ViewPager { return super.onTouchEvent( swapXY( ev ) ); } + public boolean isVerticalMode() { + return true; + } + } \ No newline at end of file diff --git project/src/main/java/gcandroid/support/v4/view/ViewPager.java project/src/main/java/gcandroid/support/v4/view/ViewPager.java index 20e1448..4ae2d3c 100644 --- project/src/main/java/gcandroid/support/v4/view/ViewPager.java +++ project/src/main/java/gcandroid/support/v4/view/ViewPager.java ## -205,6 +205,7 ## public class ViewPager extends ViewGroup { private int mMaximumVelocity; private int mFlingDistance; private int mCloseEnough; + private boolean mInvertedVelocityTrackerInVerticalMode = false; // If the pager is at least this close to its final position, complete the scroll // on touch down and let the user interact with the content inside instead of ## -391,6 +392,21 ## public class ViewPager extends ViewGroup { ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } + + // tell if the velocity tracker is inverted in vertical mode (most probably uses MotionEvent.getAxisValue instead of MotionEvent.getX but + // I can't change these values nor inherit from MotionEvent) + VelocityTracker vt = VelocityTracker.obtain(); + long time = SystemClock.uptimeMillis(); + MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + vt.addMovement(ev); + ev.recycle(); + ev = MotionEvent.obtain(time, time + 10, MotionEvent.ACTION_MOVE, 10, 0, 0); + ev.setLocation( 0, 10 ); + vt.addMovement(ev); + ev.recycle(); + vt.computeCurrentVelocity(1000, mMaximumVelocity); + mInvertedVelocityTrackerInVerticalMode = vt.getYVelocity() == 0; + vt.recycle(); } #Override ## -2027,6 +2043,10 ## public class ViewPager extends ViewGroup { return mIsBeingDragged; } + public boolean isVerticalMode() { + return false; + } + #Override public boolean onTouchEvent(MotionEvent ev) { if (mFakeDragging) { ## -2111,8 +2131,9 ## public class ViewPager extends ViewGroup { if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( - velocityTracker, mActivePointerId); + int initialVelocity = (int) ( isVerticalMode() && mInvertedVelocityTrackerInVerticalMode ? VelocityTrackerCompat.getYVelocity( velocityTracker, mActivePointerId ) + : VelocityTrackerCompat.getXVelocity( velocityTracker, mActivePointerId ) ); + mPopulatePending = true; final int width = getClientWidth(); final int scrollX = getScrollX();
EditText setCompoundDrawablesWithIntrinsicBounds() not working after setError()
I have an custom widget which has an clear button set as the right drawable using setCompoundDrawablesWithIntrinsicBounds() method. Now I need to add errors to the widget and I've tried to use the default functionality and rely on the setError() method. The issue I'm having is that after I set the error, my right drawable won't be shown again, even if I set it again afterwards. Does anyone encountered this issue and maybe found an solution to the problem? Thank you. LE: I forgot to mention that if I use setError("My error hint text", null) instead of setError("Please choose an valid destination!") everything works fine, but as you can guess the error icon won't be shown. LLE: This is my custom class which handles the display of the clear icon and the action for it. /** * Class of {#link android.widget.AutoCompleteTextView} that includes a clear (dismiss / close) button with a * OnClearListener to handle the event of clicking the button * <br/> * Created by ionut on 26.04.2016. */ public class ClearableAutoCompleteTextView extends AppCompatAutoCompleteTextView implements View.OnTouchListener { /** * The time(in milliseconds) used for transitioning between the supported states. */ private static final int TRANSITION_TIME = 500; /** * Flag for hide transition. */ private static final int TRANSITION_HIDE = 101; /** * Flag for show transition. */ private static final int TRANSITION_SHOW = 102; /** * Image representing the clear button. (will always be set to the right of the view). */ #DrawableRes private static int mImgClearButtonRes = R.drawable.ic_clear_dark; /** * Task with the role of showing the clear action button. */ private final Runnable showClearButtonRunnable = new Runnable() { #Override public void run() { Message msg = transitionHandler.obtainMessage(TRANSITION_SHOW); msg.sendToTarget(); } }; /** * Task with the role of hiding the clear action button. */ private final Runnable hideClearButtonRunnable = new Runnable() { #Override public void run() { Message msg = transitionHandler.obtainMessage(TRANSITION_HIDE); msg.sendToTarget(); } }; /** * Flag indicating if the default clear functionality should be disabled. */ private boolean mDisableDefaultFunc; // The default clear listener which will clear the input of any text private OnClearListener mDefaultClearListener = new OnClearListener() { #Override public void onClear() { getEditableText().clear(); setText(null); } }; /** * Touch listener which will receive any touch events that this view handles. */ private OnTouchListener mOnTouchListener; /** * Custom listener which will be notified when an clear event was made from the clear button. */ private OnClearListener onClearListener; /** * Transition drawable used when showing the clear action. */ private TransitionDrawable mTransitionClearButtonShow = null; /** * Transition drawable used when hiding the clear action. */ private TransitionDrawable mTransitionClearButtonHide = null; private AtomicBoolean isTransitionInProgress = new AtomicBoolean(false); private final Handler transitionHandler = new Handler(Looper.getMainLooper()) { #Override public void handleMessage(Message msg) { switch (msg.what) { case TRANSITION_HIDE: setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); isTransitionInProgress.set(false); break; case TRANSITION_SHOW: setCompoundDrawablesWithIntrinsicBounds(0, 0, mImgClearButtonRes, 0); isTransitionInProgress.set(false); break; default: super.handleMessage(msg); } } }; /* Required methods, not used in this implementation */ public ClearableAutoCompleteTextView(Context context) { super(context); init(); } /* Required methods, not used in this implementation */ public ClearableAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } /* Required methods, not used in this implementation */ public ClearableAutoCompleteTextView(Context context, AttributeSet attrs) { super(context, attrs); init(); } #Override public boolean onTouch(View v, MotionEvent event) { if (getCompoundDrawables()[2] == null) { // Pass the touch event to other listeners, if none, the normal flow is resumed return null != mOnTouchListener && mOnTouchListener.onTouch(v, event); } // React only when the UP event is detected if (event.getAction() != MotionEvent.ACTION_UP) { return false; } // Detect the clear button area of touch int x = (int) event.getX(); int y = (int) event.getY(); int left = getWidth() - getPaddingRight() - getCompoundDrawables()[2].getIntrinsicWidth(); int right = getWidth(); boolean tappedX = x >= left && x <= right && y >= 0 && y <= (getBottom() - getTop()); if (tappedX) { // Allow clear events only when the transition is not in progress if (!isTransitionInProgress.get()) { if (!mDisableDefaultFunc) { // Call the default functionality only if it wasn't disabled mDefaultClearListener.onClear(); } if (null != onClearListener) { // Call the custom clear listener so that any member listening is notified of the clear event onClearListener.onClear(); } } } // Pass the touch event to other listeners, if none, the normal flow is resumed return null != mOnTouchListener && mOnTouchListener.onTouch(v, event); } #Override public void setOnTouchListener(OnTouchListener l) { // Instead of using the super, we manually handle the touch event (only one listener can exist normally at a // time) mOnTouchListener = l; } private void init() { // Set the bounds of the button setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); if (getCompoundDrawablePadding() == 0) { // We want to have some default padding, in case no one is specified in the xml setCompoundDrawablePadding(Dimensions.dpToPx(getContext(), 5f)); } enableTransitionClearButton(); // if the clear button is pressed, fire up the handler. Otherwise do nothing super.setOnTouchListener(this); } #Override public void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(state); updateClearButton(); } #Override protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { super.onTextChanged(text, start, lengthBefore, lengthAfter); updateClearButton(); } /** * When hiding/showing the clear button an transition drawable will be used instead of the default drawable res. */ public void enableTransitionClearButton() { mTransitionClearButtonShow = (TransitionDrawable) ContextCompat.getDrawable(getContext(), R.drawable.ic_clear_fade_in); mTransitionClearButtonHide = (TransitionDrawable) ContextCompat.getDrawable(getContext(), R.drawable.ic_clear_fade_out); mTransitionClearButtonShow.setCrossFadeEnabled(true); mTransitionClearButtonHide.setCrossFadeEnabled(true); } /** * When hiding/showing the clear button the default drawable res will be used instead of the transition drawable. */ #SuppressWarnings("unused") public void disableTransitionClearButton() { mTransitionClearButtonShow = null; isTransitionInProgress.set(false); transitionHandler.removeCallbacks(hideClearButtonRunnable); transitionHandler.removeCallbacks(showClearButtonRunnable); } /** * Set an custom listener which will get notified when an clear event was triggered from the clear button. * * #param clearListener * The listener * #param disableDefaultFunc * {#code true} to disable the default clear functionality, usually meaning it will be * handled by the * calling member. {#code false} allow the default functionality of clearing the input. * * #see #setOnClearListener(OnClearListener) */ public void setOnClearListener(final OnClearListener clearListener, boolean disableDefaultFunc) { this.onClearListener = clearListener; this.mDisableDefaultFunc = disableDefaultFunc; } /** * Set an custom listener which will get notified when an clear event was triggered from the clear button. * * #param clearListener * The listener * * #see #setOnClearListener(OnClearListener, boolean) */ public void setOnClearListener(final OnClearListener clearListener) { setOnClearListener(clearListener, false); } /** * Disable the default functionality of the clear event - calling this won't allow for the input to be * automatically * cleared (it will give the ability to make custom implementations and react to the event before the clear). */ #SuppressWarnings("unused") public void disableDefaultClearFunctionality() { mDisableDefaultFunc = true; } /** * Enable the default functionality of the clear event. Will automatically clear the input when the clear button is * clicked. */ #SuppressWarnings("unused") public void enableDefaultClearFunctionality() { mDisableDefaultFunc = false; } /** * Automatically show/hide the clear button based on the current input detected. * <br/> * If there is no input the clear button will be hidden. If there is input detected the clear button will be shown. */ public void updateClearButton() { if (isEmpty()) { hideClearButton(); } else { showClearButton(); } } private boolean isEmpty() { if (null == getText()) { // Invalid editable text return true; } else if (TextUtils.isEmpty(getText().toString())) { // Empty return true; } else if (TextUtils.isEmpty(getText().toString().trim())) { // White spaces only return true; } return false; } /** * Hide the clear button. * <br/> * If an transition drawable was provided, it will be used to create an fade out effect, otherwise the default * drawable resource will be used. */ public void hideClearButton() { if (getCompoundDrawables()[2] == null) { // The clear button was already hidden - do nothing return; } if (null == mTransitionClearButtonHide) { setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); return; } mTransitionClearButtonHide.resetTransition(); isTransitionInProgress.set(true); mTransitionClearButtonHide.startTransition(TRANSITION_TIME); setCompoundDrawablesWithIntrinsicBounds(null, null, mTransitionClearButtonHide, null); transitionHandler.removeCallbacks(showClearButtonRunnable); transitionHandler.removeCallbacks(hideClearButtonRunnable); transitionHandler.postDelayed(hideClearButtonRunnable, TRANSITION_TIME); } /** * Show the clear button. * <br/> * If an transition drawable was provided, it will be used to create an fade in effect, otherwise the default * drawable resource will be used. */ public void showClearButton() { if (getCompoundDrawables()[2] != null) { // The clear button was already set - do nothing return; } if (null == mTransitionClearButtonShow) { setCompoundDrawablesWithIntrinsicBounds(0, 0, mImgClearButtonRes, 0); return; } isTransitionInProgress.set(true); mTransitionClearButtonShow.startTransition(TRANSITION_TIME); setCompoundDrawablesWithIntrinsicBounds(null, null, mTransitionClearButtonShow, null); transitionHandler.removeCallbacks(hideClearButtonRunnable); transitionHandler.removeCallbacks(showClearButtonRunnable); transitionHandler.postDelayed(showClearButtonRunnable, TRANSITION_TIME); } /** * Custom contract which is used to notify any listeners that an clear event was triggered. */ public interface OnClearListener { /** * Clear event from the clear button triggered. */ void onClear(); } } When I want to display the error I use the following command: // Enable and show the error mClearView.requestFocus(); // we need to request focus, otherwise the error won't be shown mClearView.setError("Please choose an valid destination!", null); If I use: mClearView.setError("Please choose an valid destination!"), so that the error icon to be shown, the clear button won't be shown anymore after this.
Since the documentation of setCompoundDrawablesWithIntrinsicBounds() (not so) clearly states that subsequent calls should overwrite the previously set drawables, and the documentation of setError() says the following: Sets the right-hand compound drawable of the TextView to the "error" icon and sets an error message that will be displayed in a popup when the TextView has focus. I assume this is a bug. Calling setError(null) and setting the previously set drawables to null before setting your new drawable is a possible workaround: void setError(String error) { editText.setError(error); } void setCompoundDrawableRight(Drawable rightDrawable) { editText.setError(null); editText.setCompoundDrawables(null, null, null, null); editText.setCompoundDrawablesWithIntrinsicBounds(null, null, rightDrawable, null); }
Pull to refresh in Android without popular library
I want to implement pull-to-refresh in Android app, but I don't want to use pull-to-refresh library which is available on the internet because it is too slow for the gridView I am using. So I want to implement it by hand, do you know how to do this? Or which methods should I use from GridView?
PullRefreshContainerView.java import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; /** * A container for a ListView that can be pulled to refresh. * This will create a ListView and refresh header automatically, but you can * customize them by using {#link #setList(ListView)} and {#link #setRefreshHeader(View, int)} * <p> * To use, put this where you would normally put your ListView. Since this does not extend * ListView, you must use {#link #getList()} to modify the list or provide your own. * <p> * To get the actions of the list, use a {#link OnChangeStateListener} with {#link #setOnChangeStateListener(OnChangeStateListener)}. * If you want to change how the refresh header looks, you should do it during these state changes. */ public class PullRefreshContainerView extends LinearLayout { /** * Interface for listening to when the refresh container changes state. */ public interface OnChangeStateListener { /** * Notifies a listener when the refresh view's state changes. * #param container The container that contains the header * #param state The state of the header. May be STATE_IDLE, STATE_READY, * or STATE_REFRESHING. */ public void onChangeState(PullRefreshContainerView container, int state); } /** * State of the refresh header when it is doing nothing or being pulled down slightly. */ public static final int STATE_IDLE = 0; /** * State of the refresh header when it has been pulled down but not enough to start refreshing, and * has not yet been released. */ public static final int STATE_PULL = 1; /** * State of the refresh header when it has been pulled down enough to start refreshing, but * has not yet been released. */ public static final int STATE_RELEASE = 2; /** * State of the refresh header when the list should be refreshing. */ public static final int STATE_LOADING = 3; private LinearLayout mHeaderContainer; private View mHeaderView; private ListView mList; private int mState; private OnChangeStateListener mOnChangeStateListener; private int REFRESH_VIEW_HEIGHT = 60; /** * Creates a new pull to refresh container. * * #param context the application context */ public PullRefreshContainerView(Context context) { super(context); init(context); } /** * Creates a new pull to refresh container. * * #param context the application context * #param attrs the XML attribute set */ public PullRefreshContainerView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } /** * Creates a new pull to refresh container. * * #param context the application context * #param attrs the XML attribute set * #param defStyle the style for this view */ public PullRefreshContainerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); init(context); } private void init(Context context) { mState = STATE_IDLE; // Start out as idle. float densityFactor = context.getResources().getDisplayMetrics().density; REFRESH_VIEW_HEIGHT *= densityFactor; // We don't want to see the fading edge on the container. setVerticalFadingEdgeEnabled(false); setVerticalScrollBarEnabled(false); setOrientation(LinearLayout.VERTICAL); // Set the default list and header. mHeaderContainer = new LinearLayout(context); addView(mHeaderContainer); setRefreshViewHeight(1); TextView headerView = new TextView(context); headerView.setText("Default refresh header."); setRefreshHeader(headerView); ListView list = new ListView(context); setList(list); } private boolean mScrollingList = true; private float mInterceptY; private int mLastMotionY; #Override public boolean dispatchTouchEvent (MotionEvent ev) { float oldLastY = mInterceptY; mInterceptY = ev.getY(); if (mState == STATE_LOADING) { return super.dispatchTouchEvent(ev); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastMotionY = (int) ev.getY(); mScrollingList = true; return super.dispatchTouchEvent(ev); case MotionEvent.ACTION_MOVE: if (mList.getFirstVisiblePosition() == 0 && (mList.getChildCount() == 0 || mList.getChildAt(0).getTop() == 0)) { if ((mInterceptY - oldLastY > 5) || (mState == STATE_PULL) || (mState == STATE_RELEASE)) { mScrollingList = false; applyHeaderHeight(ev); return true; } else { mScrollingList = true; return super.dispatchTouchEvent(ev); } } else if (mScrollingList) { return super.dispatchTouchEvent(ev); } else { return super.dispatchTouchEvent(ev); } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mState == STATE_RELEASE) { refresh(); } else { changeState(STATE_IDLE); } if (mScrollingList) { return super.dispatchTouchEvent(ev); } else { return true; } default: return super.dispatchTouchEvent(ev); } } private void applyHeaderHeight(MotionEvent ev) { final int historySize = ev.getHistorySize(); if (historySize > 0) { for (int h = 0; h < historySize; h++) { int historicalY = (int) (ev.getHistoricalY(h)); updateRefreshView(historicalY - mLastMotionY); } } else { int historicalY = (int) ev.getY(); updateRefreshView(historicalY - mLastMotionY); } } private void updateRefreshView(int height) { if (height <= 0) { return; } if ((REFRESH_VIEW_HEIGHT/4 <= mCurRefreshViewHeight) && (mCurRefreshViewHeight < REFRESH_VIEW_HEIGHT)) { setRefreshViewHeight(height); changeState(STATE_PULL); } else if (mCurRefreshViewHeight >= REFRESH_VIEW_HEIGHT) { if (height > REFRESH_VIEW_HEIGHT) { height = (int) (REFRESH_VIEW_HEIGHT + (height - REFRESH_VIEW_HEIGHT) * REFRESH_VIEW_HEIGHT * 1.0f/height); } setRefreshViewHeight(height); changeState(STATE_RELEASE); } else { setRefreshViewHeight(height); } } private int mCurRefreshViewHeight = 60; private void setRefreshViewHeight(int height) { if (mCurRefreshViewHeight == height) { return; } if (height == 1) { mHeaderContainer.setLayoutParams(new LayoutParams(1, 1)); } else { mHeaderContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height)); } mCurRefreshViewHeight = height; } private void changeState(int state) { switch (state) { case STATE_IDLE: setRefreshViewHeight(1); break; case STATE_PULL: break; case STATE_RELEASE: break; case STATE_LOADING: setRefreshViewHeight(REFRESH_VIEW_HEIGHT); break; } mState = state; notifyStateChanged(); } /** * Sets the list to be used in this pull to refresh container. * #param list the list to use */ public void setList(ListView list) { if (mList != null) { removeView(mList); } mList = list; if (mList.getParent() != null) { ViewGroup parent = (ViewGroup) mList.getParent(); parent.removeView(mList); } mList.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); addView(mList); } /** * #return the list inside this pull to refresh container */ public ListView getList() { return mList; } /** * Sets the view to use as the refresh header. * <p /> * The header view is the view at the top that will show while the list * is refreshing. Usually, this will be a simple rectangle that says "refreshing" and the like. * <p /> * * #param headerView the view to use as the whole header. */ public void setRefreshHeader(View header) { if (mHeaderView != null) { mHeaderContainer.removeView(mHeaderView); } if (header == null) { throw new RuntimeException("Please supply a non-null header container."); } mHeaderContainer.addView(header, 0); mHeaderView = header; } public void refresh() { changeState(STATE_LOADING); } /** * Notifies the pull-to-refresh view that the refreshing is complete. * This will hide the refreshing header. */ public void completeRefresh() { changeState(STATE_IDLE); } /** * Notifies the listener that the state has changed. */ private void notifyStateChanged() { if (mOnChangeStateListener != null) { mOnChangeStateListener.onChangeState(this, mState); } } /** * #param listener the listener to be notified when the header state should change */ public void setOnChangeStateListener(OnChangeStateListener listener) { mOnChangeStateListener = listener; } } How To use UsageDemoActivity.java import java.util.ArrayList; import java.util.Timer; import java.util.TimerTask; import com.dmobile.pulltorefresh.PullRefreshContainerView.OnChangeStateListener; import com.dmobile.pulltorefresh.R; import android.app.Activity; import android.os.Bundle; import android.view.Gravity; import android.view.ViewGroup.LayoutParams; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; public class UsageDemoActivity extends Activity { private PullRefreshContainerView mContainerView; private TextView mRefreshHeader; private ListView mList; private ArrayList<String> mStrings = new ArrayList<String>(); private ArrayAdapter<String> mAdapter; /** Called when the activity is first created. */ #Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mRefreshHeader = new TextView(this); mRefreshHeader.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mRefreshHeader.setGravity(Gravity.CENTER); mRefreshHeader.setText("Pull to refresh..."); mContainerView = (PullRefreshContainerView) findViewById(R.id.container); mContainerView.setRefreshHeader(mRefreshHeader); mContainerView.setOnChangeStateListener(new OnChangeStateListener() { #Override public void onChangeState(PullRefreshContainerView container, int state) { switch (state) { case PullRefreshContainerView.STATE_IDLE: case PullRefreshContainerView.STATE_PULL: mRefreshHeader.setText("Pull to refresh..."); break; case PullRefreshContainerView.STATE_RELEASE: mRefreshHeader.setText("Release to refresh..."); break; case PullRefreshContainerView.STATE_LOADING: mRefreshHeader.setText("Loading..."); final Timer t = new Timer(); t.schedule(new TimerTask() { #Override public void run() { UsageDemoActivity.this.runOnUiThread(new Runnable() { #Override public void run() { addStrings(1); mContainerView.completeRefresh(); t.cancel(); } }); } }, 5000, 5000); break; } } }); mList = mContainerView.getList(); mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mStrings); mList.setAdapter(mAdapter); addStrings(3); } private void addStrings(int count) { int curSize = mStrings.size(); for (int i = 0; i < count; ++i) { mStrings.add("String " + (curSize + i)); } mAdapter.notifyDataSetChanged(); } } main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.dmobile.pulltorefresh.PullRefreshContainerView android:id="#+id/container" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </LinearLayout> UPDATE TO WORK WITH GRIDVIEW PullRefreshContainerView.java private void init(Context context) { mState = STATE_IDLE; // Start out as idle. float densityFactor = context.getResources().getDisplayMetrics().density; REFRESH_VIEW_HEIGHT *= densityFactor; // We don't want to see the fading edge on the container. setVerticalFadingEdgeEnabled(false); setVerticalScrollBarEnabled(false); setOrientation(LinearLayout.VERTICAL); // Set the default list and header. mHeaderContainer = new LinearLayout(context); addView(mHeaderContainer); setRefreshViewHeight(1); TextView headerView = new TextView(context); headerView.setText("Default refresh header."); setRefreshHeader(headerView); GridView grid = new GridView(context); setList(grid); } And also make related change in setListMethod();
Android: Up/down animated sliding menu
I have tried several solutions but need help. The topics below are really useful but I think I'm doing something wrong. How to set layout height/settings for both? Let's say I have 2 LinearLayout for content and bottom menu. Also I don't want the bottom menu disappeared after sliding. It should be constant there. I am using fragments for menu clicks/change views. Android: Expand/collapse animation Android animate drop down/up view proper
As my comment seemed to help, I will post the link as an answer: https://github.com/umano/AndroidSlidingUpPanel The full code cannot be pasted in StackOverflow, but the whole library will help you to achieve what you need. The 2.2 version of the Umano Android app features a sexy sliding up draggable panel for the currently playing article. This type of a panel is a common pattern also used in the Google Music app and the Rdio app. This is an open source implementation of this component that you are free to take advantage of in your apps. Umano Team <3 Open Source. <com.sothree.slidinguppaneldemo.SlidingUpPanelLayout android:id="#+id/sliding_layout" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="Main Content" android:textSize="16sp" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:text="The Awesome Sliding Up Panel" android:textSize="16sp" /> </com.sothree.slidinguppaneldemo.SlidingUpPanelLayout>
You can also try this custom view for ExpandablePanel found it somewhere when i needed to create something like this. import android.content.Context; import android.content.res.TypedArray; import android.graphics.Point; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.LinearLayout; public class ExpandablePanel extends LinearLayout { private final int mHandleId; private final int mContentId; // Contains references to the handle and content views private View mHandle; private View mContent; // Does the panel start expanded? private boolean mExpanded = false; // The height of the content when collapsed private int mCollapsedHeight = 0; // The full expanded height of the content (calculated) private int mContentHeight = 0; // How long the expand animation takes private int mAnimationDuration = 0; int height; private Context context; // Listener that gets fired onExpand and onCollapse private OnExpandListener mListener; public ExpandablePanel(Context context) { this(context, null); this.context = context; } public void setSize(int size) { this.height = size; } /** * The constructor simply validates the arguments being passed in and sets * the global variables accordingly. Required attributes are 'handle' and * 'content' */ public ExpandablePanel(Context context, AttributeSet attrs) { super(context, attrs); mListener = new DefaultOnExpandListener(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExpandablePanel, 0, 0); // How high the content should be in "collapsed" state mCollapsedHeight = (int) a.getDimension( R.styleable.ExpandablePanel_collapsedHeight, 0.0f); // How long the animation should take mAnimationDuration = a.getInteger( R.styleable.ExpandablePanel_animationDuration, 500); int handleId = a.getResourceId(R.styleable.ExpandablePanel_handle, 0); if (handleId == 0) { throw new IllegalArgumentException( "The handle attribute is required and must refer " + "to a valid child."); } int contentId = a.getResourceId(R.styleable.ExpandablePanel_content, 0); if (contentId == 0) { throw new IllegalArgumentException( "The content attribute is required and must " + "refer to a valid child."); } mHandleId = handleId; mContentId = contentId; a.recycle(); } // Some public setters for manipulating the // ExpandablePanel programmatically public void setOnExpandListener(OnExpandListener listener) { mListener = listener; } public void setCollapsedHeight(int collapsedHeight) { mCollapsedHeight = collapsedHeight; } public void setAnimationDuration(int animationDuration) { mAnimationDuration = animationDuration; } /** * This method gets called when the View is physically visible to the user */ #Override protected void onFinishInflate() { super.onFinishInflate(); mHandle = findViewById(mHandleId); if (mHandle == null) { throw new IllegalArgumentException( "The handle attribute is must refer to an" + " existing child."); } mContent = findViewById(mContentId); if (mContent == null) { throw new IllegalArgumentException( "The content attribute must refer to an" + " existing child."); } // This changes the height of the content such that it // starts off collapsed android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams(); lp.height = mCollapsedHeight; mContent.setLayoutParams(lp); // Set the OnClickListener of the handle view mHandle.setOnClickListener(new PanelToggler()); } /** * This is where the magic happens for measuring the actual (un-expanded) * height of the content. If the actual height is less than the * collapsedHeight, the handle will be hidden. */ #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // First, measure how high content wants to be mContent.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); mContentHeight = mContent.getMeasuredHeight(); Log.v("cHeight", mContentHeight + ""); Log.v("cCollapseHeight", mCollapsedHeight + ""); if (mContentHeight < mCollapsedHeight) { mHandle.setVisibility(View.GONE); } else { mHandle.setVisibility(View.VISIBLE); } // Then let the usual thing happen super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** * This is the on click listener for the handle. It basically just creates a * new animation instance and fires animation. */ private class PanelToggler implements OnClickListener { public void onClick(View v) { Animation a; if (mExpanded) { a = new ExpandAnimation(mContentHeight, mCollapsedHeight); mListener.onCollapse(mHandle, mContent); } else { a = new ExpandAnimation(mCollapsedHeight, mContentHeight); mListener.onExpand(mHandle, mContent); } a.setDuration(mAnimationDuration); mContent.startAnimation(a); mExpanded = !mExpanded; } } /** * This is a private animation class that handles the expand/collapse * animations. It uses the animationDuration attribute for the length of * time it takes. */ private class ExpandAnimation extends Animation { private final int mStartHeight; private final int mDeltaHeight; public ExpandAnimation(int startHeight, int endHeight) { mStartHeight = startHeight; mDeltaHeight = endHeight - startHeight; } #Override protected void applyTransformation(float interpolatedTime, Transformation t) { android.view.ViewGroup.LayoutParams lp = mContent.getLayoutParams(); lp.height = (int) (mStartHeight + mDeltaHeight * interpolatedTime); mContent.setLayoutParams(lp); } #Override public boolean willChangeBounds() { return true; } } /** * Simple OnExpandListener interface */ public interface OnExpandListener { public void onExpand(View handle, View content); public void onCollapse(View handle, View content); } private class DefaultOnExpandListener implements OnExpandListener { public void onCollapse(View handle, View content) { } public void onExpand(View handle, View content) { } } }
Handling click events on a drawable within an EditText
I have added an image right of the text in an EditText widget, using the following XML: <EditText android:id="#+id/txtsearch" ... android:layout_gravity="center_vertical" android:background="#layout/shape" android:hint="Enter place,city,state" android:drawableRight="#drawable/cross" /> But I want to clear the EditText when the embedded image is clicked. How can I do this?
Actually you don't need to extend any class. Let's say I have an EditText editComment with a drawableRight editComment.setOnTouchListener(new OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { final int DRAWABLE_LEFT = 0; final int DRAWABLE_TOP = 1; final int DRAWABLE_RIGHT = 2; final int DRAWABLE_BOTTOM = 3; if(event.getAction() == MotionEvent.ACTION_UP) { if(event.getRawX() >= (editComment.getRight() - editComment.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { // your action here return true; } } return false; } }); we getRawX() because we want to get the actual position of touch on screen, not relative to parent. To get left side click if(event.getRawX() <= (editComment.getCompoundDrawables()[DRAWABLE_LEFT].getBounds().width()))
Very, very good, thanks to everyone who contributed to this discussion. So if you don't want to deal with inconvenience of extending the class you can do the following (implemented for the right drawable only) this.keyword = (AutoCompleteTextView) findViewById(R.id.search); this.keyword.setOnTouchListener(new RightDrawableOnTouchListener(keyword) { #Override public boolean onDrawableTouch(final MotionEvent event) { return onClickSearch(keyword,event); } }); private boolean onClickSearch(final View view, MotionEvent event) { // do something event.setAction(MotionEvent.ACTION_CANCEL); return false; } And here's bare-bone listener implementation based on #Mark's answer public abstract class RightDrawableOnTouchListener implements OnTouchListener { Drawable drawable; private int fuzz = 10; /** * #param keyword */ public RightDrawableOnTouchListener(TextView view) { super(); final Drawable[] drawables = view.getCompoundDrawables(); if (drawables != null && drawables.length == 4) this.drawable = drawables[2]; } /* * (non-Javadoc) * * #see android.view.View.OnTouchListener#onTouch(android.view.View, android.view.MotionEvent) */ #Override public boolean onTouch(final View v, final MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN && drawable != null) { final int x = (int) event.getX(); final int y = (int) event.getY(); final Rect bounds = drawable.getBounds(); if (x >= (v.getRight() - bounds.width() - fuzz) && x <= (v.getRight() - v.getPaddingRight() + fuzz) && y >= (v.getPaddingTop() - fuzz) && y <= (v.getHeight() - v.getPaddingBottom()) + fuzz) { return onDrawableTouch(event); } } return false; } public abstract boolean onDrawableTouch(final MotionEvent event); }
Consider the following. It's not the most elegant solution but it works, I just tested it. Create a customized EditText class CustomEditText.java: import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.EditText; public class CustomEditText extends EditText { private Drawable dRight; private Rect rBounds; public CustomEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public CustomEditText(Context context, AttributeSet attrs) { super(context, attrs); } public CustomEditText(Context context) { super(context); } #Override public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { if(right !=null) { dRight = right; } super.setCompoundDrawables(left, top, right, bottom); } #Override public boolean onTouchEvent(MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_UP && dRight!=null) { rBounds = dRight.getBounds(); final int x = (int)event.getX(); final int y = (int)event.getY(); //System.out.println("x:/y: "+x+"/"+y); //System.out.println("bounds: "+bounds.left+"/"+bounds.right+"/"+bounds.top+"/"+bounds.bottom); //check to make sure the touch event was within the bounds of the drawable if(x>=(this.getRight()-rBounds.width()) && x<=(this.getRight()-this.getPaddingRight()) && y>=this.getPaddingTop() && y<=(this.getHeight()-this.getPaddingBottom())) { //System.out.println("touch"); this.setText(""); event.setAction(MotionEvent.ACTION_CANCEL);//use this to prevent the keyboard from coming up } } return super.onTouchEvent(event); } #Override protected void finalize() throws Throwable { dRight = null; rBounds = null; super.finalize(); } } Change your layout XML to this (where com.example is your actual project package name): <com.example.CustomEditText android:id="#+id/txtsearch" … android:layout_gravity="center_vertical" android:background="#layout/shape" android:hint="Enter place,city,state" android:drawableRight="#drawable/cross" /> Finally, add this (or something similar) to your activity: … CustomEditText et = (CustomEditText) this.findViewById(R.id.txtsearch); … I might be a bit off with the calculation of the touch bounds for the nested drawable but you get the idea. I hope this helps.
I created a useful abstract class DrawableClickListener which implements OnTouchListener. In addition to the DrawableClickListener class, I also created 4 additional abstract classes which extend the DrawableClickListener class and handle the clicking of the drawable area for the correct quadrant. LeftDrawableClickListener TopDrawableClickListener RightDrawableClickListener BottomDrawableClickListener Point to Consider One thing to consider is that the images are not resized if done this way; thus the images must be scaled correctly before being put into the res/drawable folder(s). If you define a LinearLayout containing an ImageView and a TextView, it's a lot easier to manipulate the size of the image being displayed. activity_my.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="#+id/myTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="replace this with a variable" android:textSize="30sp" android:drawableLeft="#drawable/my_left_image" android:drawableRight="#drawable/my_right_image" android:drawablePadding="9dp" /> </RelativeLayout> MyActivity.java package com.company.project.core; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MyActivity extends Activity { #Override protected void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ); setContentView( R.layout.activity_my ); final TextView myTextView = (TextView) this.findViewById( R.id.myTextView ); myTextView.setOnTouchListener( new DrawableClickListener.LeftDrawableClickListener(myTextView) { #Override public boolean onDrawableClick() { // TODO : insert code to perform on clicking of the LEFT drawable image... return true; } } ); myTextView.setOnTouchListener( new DrawableClickListener.RightDrawableClickListener(myTextView) { #Override public boolean onDrawableClick() { // TODO : insert code to perform on clicking of the RIGHT drawable image... return true; } } ); } } DrawableClickListener.java package com.company.project.core; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.TextView; /** * This class can be used to define a listener for a compound drawable. * * #author Matthew Weiler * */ public abstract class DrawableClickListener implements OnTouchListener { /* PUBLIC CONSTANTS */ /** * This represents the left drawable. * */ public static final int DRAWABLE_INDEX_LEFT = 0; /** * This represents the top drawable. * */ public static final int DRAWABLE_INDEX_TOP = 1; /** * This represents the right drawable. * */ public static final int DRAWABLE_INDEX_RIGHT = 2; /** * This represents the bottom drawable. * */ public static final int DRAWABLE_INDEX_BOTTOM = 3; /** * This stores the default value to be used for the * {#link DrawableClickListener#fuzz}. * */ public static final int DEFAULT_FUZZ = 10; /* PRIVATE VARIABLES */ /** * This stores the number of pixels of "fuzz" that should be * included to account for the size of a finger. * */ private final int fuzz; /** * This will store a reference to the {#link Drawable}. * */ private Drawable drawable = null; /* CONSTRUCTORS */ /** * This will create a new instance of a {#link DrawableClickListener} * object. * * #param view * The {#link TextView} that this {#link DrawableClickListener} * is associated with. * #param drawableIndex * The index of the drawable that this * {#link DrawableClickListener} pertains to. * <br /> * <i>use one of the values: * <b>DrawableOnTouchListener.DRAWABLE_INDEX_*</b></i> */ public DrawableClickListener( final TextView view, final int drawableIndex ) { this( view, drawableIndex, DrawableClickListener.DEFAULT_FUZZ ); } /** * This will create a new instance of a {#link DrawableClickListener} * object. * * #param view * The {#link TextView} that this {#link DrawableClickListener} * is associated with. * #param drawableIndex * The index of the drawable that this * {#link DrawableClickListener} pertains to. * <br /> * <i>use one of the values: * <b>DrawableOnTouchListener.DRAWABLE_INDEX_*</b></i> * #param fuzzOverride * The number of pixels of "fuzz" that should be * included to account for the size of a finger. */ public DrawableClickListener( final TextView view, final int drawableIndex, final int fuzz ) { super(); this.fuzz = fuzz; final Drawable[] drawables = view.getCompoundDrawables(); if ( drawables != null && drawables.length == 4 ) { this.drawable = drawables[drawableIndex]; } } /* OVERRIDDEN PUBLIC METHODS */ #Override public boolean onTouch( final View v, final MotionEvent event ) { if ( event.getAction() == MotionEvent.ACTION_DOWN && drawable != null ) { final int x = (int) event.getX(); final int y = (int) event.getY(); final Rect bounds = drawable.getBounds(); if ( this.isClickOnDrawable( x, y, v, bounds, this.fuzz ) ) { return this.onDrawableClick(); } } return false; } /* PUBLIC METHODS */ /** * * */ public abstract boolean isClickOnDrawable( final int x, final int y, final View view, final Rect drawableBounds, final int fuzz ); /** * This method will be fired when the drawable is touched/clicked. * * #return * <code>true</code> if the listener has consumed the event; * <code>false</code> otherwise. * */ public abstract boolean onDrawableClick(); /* PUBLIC CLASSES */ /** * This class can be used to define a listener for a <b>LEFT</b> compound * drawable. * */ public static abstract class LeftDrawableClickListener extends DrawableClickListener { /* CONSTRUCTORS */ /** * This will create a new instance of a * {#link LeftDrawableClickListener} object. * * #param view * The {#link TextView} that this * {#link LeftDrawableClickListener} is associated with. */ public LeftDrawableClickListener( final TextView view ) { super( view, DrawableClickListener.DRAWABLE_INDEX_LEFT ); } /** * This will create a new instance of a * {#link LeftDrawableClickListener} object. * * #param view * The {#link TextView} that this * {#link LeftDrawableClickListener} is associated with. * #param fuzzOverride * The number of pixels of "fuzz" that should be * included to account for the size of a finger. */ public LeftDrawableClickListener( final TextView view, final int fuzz ) { super( view, DrawableClickListener.DRAWABLE_INDEX_LEFT, fuzz ); } /* PUBLIC METHODS */ public boolean isClickOnDrawable( final int x, final int y, final View view, final Rect drawableBounds, final int fuzz ) { if ( x >= ( view.getPaddingLeft() - fuzz ) ) { if ( x <= ( view.getPaddingLeft() + drawableBounds.width() + fuzz ) ) { if ( y >= ( view.getPaddingTop() - fuzz ) ) { if ( y <= ( view.getHeight() - view.getPaddingBottom() + fuzz ) ) { return true; } } } } return false; } } /** * This class can be used to define a listener for a <b>TOP</b> compound * drawable. * */ public static abstract class TopDrawableClickListener extends DrawableClickListener { /* CONSTRUCTORS */ /** * This will create a new instance of a {#link TopDrawableClickListener} * object. * * #param view * The {#link TextView} that this * {#link TopDrawableClickListener} is associated with. */ public TopDrawableClickListener( final TextView view ) { super( view, DrawableClickListener.DRAWABLE_INDEX_TOP ); } /** * This will create a new instance of a {#link TopDrawableClickListener} * object. * * #param view * The {#link TextView} that this * {#link TopDrawableClickListener} is associated with. * #param fuzzOverride * The number of pixels of "fuzz" that should be * included to account for the size of a finger. */ public TopDrawableClickListener( final TextView view, final int fuzz ) { super( view, DrawableClickListener.DRAWABLE_INDEX_TOP, fuzz ); } /* PUBLIC METHODS */ public boolean isClickOnDrawable( final int x, final int y, final View view, final Rect drawableBounds, final int fuzz ) { if ( x >= ( view.getPaddingLeft() - fuzz ) ) { if ( x <= ( view.getWidth() - view.getPaddingRight() + fuzz ) ) { if ( y >= ( view.getPaddingTop() - fuzz ) ) { if ( y <= ( view.getPaddingTop() + drawableBounds.height() + fuzz ) ) { return true; } } } } return false; } } /** * This class can be used to define a listener for a <b>RIGHT</b> compound * drawable. * */ public static abstract class RightDrawableClickListener extends DrawableClickListener { /* CONSTRUCTORS */ /** * This will create a new instance of a * {#link RightDrawableClickListener} object. * * #param view * The {#link TextView} that this * {#link RightDrawableClickListener} is associated with. */ public RightDrawableClickListener( final TextView view ) { super( view, DrawableClickListener.DRAWABLE_INDEX_RIGHT ); } /** * This will create a new instance of a * {#link RightDrawableClickListener} object. * * #param view * The {#link TextView} that this * {#link RightDrawableClickListener} is associated with. * #param fuzzOverride * The number of pixels of "fuzz" that should be * included to account for the size of a finger. */ public RightDrawableClickListener( final TextView view, final int fuzz ) { super( view, DrawableClickListener.DRAWABLE_INDEX_RIGHT, fuzz ); } /* PUBLIC METHODS */ public boolean isClickOnDrawable( final int x, final int y, final View view, final Rect drawableBounds, final int fuzz ) { if ( x >= ( view.getWidth() - view.getPaddingRight() - drawableBounds.width() - fuzz ) ) { if ( x <= ( view.getWidth() - view.getPaddingRight() + fuzz ) ) { if ( y >= ( view.getPaddingTop() - fuzz ) ) { if ( y <= ( view.getHeight() - view.getPaddingBottom() + fuzz ) ) { return true; } } } } return false; } } /** * This class can be used to define a listener for a <b>BOTTOM</b> compound * drawable. * */ public static abstract class BottomDrawableClickListener extends DrawableClickListener { /* CONSTRUCTORS */ /** * This will create a new instance of a * {#link BottomDrawableClickListener} object. * * #param view * The {#link TextView} that this * {#link BottomDrawableClickListener} is associated with. */ public BottomDrawableClickListener( final TextView view ) { super( view, DrawableClickListener.DRAWABLE_INDEX_BOTTOM ); } /** * This will create a new instance of a * {#link BottomDrawableClickListener} object. * * #param view * The {#link TextView} that this * {#link BottomDrawableClickListener} is associated with. * #param fuzzOverride * The number of pixels of "fuzz" that should be * included to account for the size of a finger. */ public BottomDrawableClickListener( final TextView view, final int fuzz ) { super( view, DrawableClickListener.DRAWABLE_INDEX_BOTTOM, fuzz ); } /* PUBLIC METHODS */ public boolean isClickOnDrawable( final int x, final int y, final View view, final Rect drawableBounds, final int fuzz ) { if ( x >= ( view.getPaddingLeft() - fuzz ) ) { if ( x <= ( view.getWidth() - view.getPaddingRight() + fuzz ) ) { if ( y >= ( view.getHeight() - view.getPaddingBottom() - drawableBounds.height() - fuzz ) ) { if ( y <= ( view.getHeight() - view.getPaddingBottom() + fuzz ) ) { return true; } } } } return false; } } }
Kotlin is a great language where each class could be extended with new methods. Lets introduce new method for EditText class which will catch clicks to right drawable. fun EditText.onRightDrawableClicked(onClicked: (view: EditText) -> Unit) { this.setOnTouchListener { v, event -> var hasConsumed = false if (v is EditText) { if (event.x >= v.width - v.totalPaddingRight) { if (event.action == MotionEvent.ACTION_UP) { onClicked(this) } hasConsumed = true } } hasConsumed } } You can see it takes callback function as argument which is called when user clicks to right drawable. val username = findViewById<EditText>(R.id.username_text) username.onRightDrawableClicked { it.text.clear() }
Its very simple. Lets say you have a drawable on left side of your EditText 'txtsearch'. Following will do the trick. EditText txtsearch = (EditText) findViewById(R.id.txtsearch); txtsearch.setOnTouchListener(new OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_UP) { if(event.getRawX() <= txtsearch.getTotalPaddingLeft()) { // your action for drawable click event return true; } } return false; } }); If you want for right drawable change the if statement to: if(event.getRawX() >= txtsearch.getRight() - txtsearch.getTotalPaddingRight()) Similarly, you can do it for all compound drawables. txtsearch.getTotalPaddingTop() txtsearch.getTotalPaddingBottom() This method call returns all the padding on that side including any drawables. You can use this even for TextView, Button etc. Click here for reference from android developer site.
I think it is much more easier if we use some tricks :) Create a image button with your icon and set its background color to be transparent. Put the image button on the EditText and of coz the right hand side Implement the onclick listener of the button to execute your function Done
That last contribution's use of contains(x,y) won't work directly on the result of getBounds() (except, by coincidence, when using "left" drawables). The getBounds method only provides the Rect defining points of the drawable item normalized with origin at 0,0 - so, you actually need to do the math of the original post to find out if the click is in the area of the drawable in the context of the containing EditText's dimensions, but change it for top, right, left etc. Alternatively you could describe a Rect that has coordinates actually relative to its position in the EditText container and use contains(), although in the end you're doing the same math. Combining them both gives you a pretty complete solution, I only added an instance attribute consumesEvent that lets the API user decide if the click event should be passed on or not by using its result to set ACTION_CANCEL or not. Also, I can't see why the bounds and actionX, actionY values are instance attributes rather than just local on the stack. Here's a cutout from an implementation based on the above that I put together. It fixes an issue that to properly consume the event you need to return false. It adds a "fuzz" factor to. In my use case of a Voice control icon in an EditText field, I found it hard to click, so the fuzz increases the effective bounds that are considered clicking the drawable. For me 15 worked well. I only needed drawableRight so I didn't plug the math in the others, to save some space, but you see the idea. package com.example.android; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.EditText; import android.graphics.Rect; import com.example.android.DrawableClickListener; public class ClickableButtonEditText extends EditText { public static final String LOG_TAG = "ClickableButtonEditText"; private Drawable drawableRight; private Drawable drawableLeft; private Drawable drawableTop; private Drawable drawableBottom; private boolean consumeEvent = false; private int fuzz = 0; private DrawableClickListener clickListener; public ClickableButtonEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public ClickableButtonEditText(Context context, AttributeSet attrs) { super(context, attrs); } public ClickableButtonEditText(Context context) { super(context); } public void consumeEvent() { this.setConsumeEvent(true); } public void setConsumeEvent(boolean b) { this.consumeEvent = b; } public void setFuzz(int z) { this.fuzz = z; } public int getFuzz() { return fuzz; } #Override public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { if (right != null) { drawableRight = right; } if (left != null) { drawableLeft = left; } super.setCompoundDrawables(left, top, right, bottom); } #Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { int x, y; Rect bounds; x = (int) event.getX(); y = (int) event.getY(); // this works for left since container shares 0,0 origin with bounds if (drawableLeft != null) { bounds = drawableLeft.getBounds(); if (bounds.contains(x - fuzz, y - fuzz)) { clickListener.onClick(DrawableClickListener.DrawablePosition.LEFT); if (consumeEvent) { event.setAction(MotionEvent.ACTION_CANCEL); return false; } } } else if (drawableRight != null) { bounds = drawableRight.getBounds(); if (x >= (this.getRight() - bounds.width() - fuzz) && x <= (this.getRight() - this.getPaddingRight() + fuzz) && y >= (this.getPaddingTop() - fuzz) && y <= (this.getHeight() - this.getPaddingBottom()) + fuzz) { clickListener.onClick(DrawableClickListener.DrawablePosition.RIGHT); if (consumeEvent) { event.setAction(MotionEvent.ACTION_CANCEL); return false; } } } else if (drawableTop != null) { // not impl reader exercise :) } else if (drawableBottom != null) { // not impl reader exercise :) } } return super.onTouchEvent(event); } #Override protected void finalize() throws Throwable { drawableRight = null; drawableBottom = null; drawableLeft = null; drawableTop = null; super.finalize(); } public void setDrawableClickListener(DrawableClickListener listener) { this.clickListener = listener; } }
I have implemented in Kotlin edPassword.setOnTouchListener { _, event -> val DRAWABLE_RIGHT = 2 val DRAWABLE_LEFT = 0 val DRAWABLE_TOP = 1 val DRAWABLE_BOTTOM = 3 if (event.action == MotionEvent.ACTION_UP) { if (event.rawX >= (edPassword.right - edPassword.compoundDrawables[DRAWABLE_RIGHT].bounds.width())) { edPassword.setText("") true } } false }
Extending on the idea by RyanM I have created a more flexible version, which supports all the drawable types (top, bottom, left, right). While the code below extends TextView, adapting it for an EditText is just a case of swapping "extends TextView" with "extends EditText". Instantiation the widget from XML is identical as in RyanM's example, bar the widget name. import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.TextView; import com.example.DrawableClickListener.DrawablePosition; public class ButtonTextView extends TextView { private Drawable drawableRight; private Drawable drawableLeft; private Drawable drawableTop; private Drawable drawableBottom; private int actionX, actionY; private DrawableClickListener clickListener; public ButtonTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public ButtonTextView(Context context, AttributeSet attrs) { super(context, attrs); } public ButtonTextView(Context context) { super(context); } #Override public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { if (right != null) { drawableRight = right; } if (left != null) { drawableLeft = left; } if (top != null) { drawableTop = top; } if (bottom != null) { drawableBottom = bottom; } super.setCompoundDrawables(left, top, right, bottom); } #Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { actionX = (int) event.getX(); actionY = (int) event.getY(); if (drawableBottom != null && drawableBottom.getBounds().contains(actionX, actionY)) { clickListener.onClick(DrawablePosition.BOTTOM); return super.onTouchEvent(event); } if (drawableTop != null && drawableTop.getBounds().contains(actionX, actionY)) { clickListener.onClick(DrawablePosition.TOP); return super.onTouchEvent(event); } if (drawableLeft != null && drawableLeft.getBounds().contains(actionX, actionY)) { clickListener.onClick(DrawablePosition.LEFT); return super.onTouchEvent(event); } if (drawableRight != null && drawableRight.getBounds().contains(actionX, actionY)) { clickListener.onClick(DrawablePosition.RIGHT); return super.onTouchEvent(event); } } return super.onTouchEvent(event); } #Override protected void finalize() throws Throwable { drawableRight = null; drawableBottom = null; drawableLeft = null; drawableTop = null; super.finalize(); } public void setDrawableClickListener(DrawableClickListener listener) { this.clickListener = listener; }} The DrawableClickListener is as simple as this: public interface DrawableClickListener { public static enum DrawablePosition { TOP, BOTTOM, LEFT, RIGHT }; public void onClick(DrawablePosition target); } And then the actual implementation: class example implements DrawableClickListener { public void onClick(DrawablePosition target) { switch (target) { case LEFT: doSomethingA(); break; case RIGHT: doSomethingB(); break; case BOTTOM: doSomethingC(); break; case TOP: doSomethingD(); break; default: break; } }} p.s.: If you don't set the listener, touching the TextView will cause a NullPointerException. You may want to add some more paranoia into the code.
its working for me, mEditTextSearch.addTextChangedListener(new TextWatcher() { #Override public void onTextChanged(CharSequence s, int start, int before, int count) { if(s.length()>0){ mEditTextSearch.setCompoundDrawablesWithIntrinsicBounds(null, null, getResources().getDrawable(android.R.drawable.ic_delete), null); }else{ mEditTextSearch.setCompoundDrawablesWithIntrinsicBounds(null, null, getResources().getDrawable(R.drawable.abc_ic_search), null); } } #Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } #Override public void afterTextChanged(Editable s) { } }); mEditTextSearch.setOnTouchListener(new OnTouchListener() { #SuppressLint("ClickableViewAccessibility") #Override public boolean onTouch(View v, MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_UP) { if(mEditTextSearch.getCompoundDrawables()[2]!=null){ if(event.getX() >= (mEditTextSearch.getRight()- mEditTextSearch.getLeft() - mEditTextSearch.getCompoundDrawables()[2].getBounds().width())) { mEditTextSearch.setText(""); } } } return false; } });
I know this is quite old, but I recently had to do something similar... After seeing how difficult this is, I came up with a much simpler solution: Create an XML layout that contains the EditText and Image Subclass FrameLayout and inflate the XML layout Add code for the click listener and any other behavior you want In my case, I needed an EditText that had the ability to clear the text with a button. I wanted it to look like SearchView, but for a number of reasons I didn't want to use that class. The example below shows how I accomplished this. Even though it doesn't have to do with focus change, the principles are the same and I figured it would be more beneficial to post actual working code than to put together an example that may not work exactly as I intended: Here is my layout: clearable_edit_text.xml <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="#+id/edit_text_field" android:layout_width="match_parent" android:layout_height="wrap_content"/> <!-- NOTE: Visibility cannot be set to "gone" or the padding won't get set properly in code --> <ImageButton android:id="#+id/edit_text_clear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right|center_vertical" android:background="#drawable/ic_cancel_x" android:visibility="invisible"/> </merge> And here is the Class that inflates that layout: ClearableEditText.java public class ClearableEditText extends FrameLayout { private boolean mPaddingSet = false; /** * Creates a new instance of this class. * #param context The context used to create the instance */ public ClearableEditText (final Context context) { this(context, null, 0); } /** * Creates a new instance of this class. * #param context The context used to create the instance * #param attrs The attribute set used to customize this instance */ public ClearableEditText (final Context context, final AttributeSet attrs) { this(context, attrs, 0); } /** * Creates a new instance of this class. * #param context The context used to create the instance * #param attrs The attribute set used to customize this instance * #param defStyle The default style to be applied to this instance */ public ClearableEditText (final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); final LayoutInflater inflater = LayoutInflater.from(context); inflater.inflate(R.layout.clearable_edit_text, this, true); } #Override protected void onFinishInflate () { super.onFinishInflate(); final EditText editField = (EditText) findViewById(R.id.edit_text_field); final ImageButton clearButton = (ImageButton) findViewById(R.id.edit_text_clear); //Set text listener so we can show/hide the close button based on whether or not it has text editField.addTextChangedListener(new TextWatcher() { #Override public void beforeTextChanged (final CharSequence charSequence, final int i, final int i2, final int i3) { //Do nothing here } #Override public void onTextChanged (final CharSequence charSequence, final int i, final int i2, final int i3) { //Do nothing here } #Override public void afterTextChanged (final Editable editable) { clearButton.setVisibility(editable.length() > 0 ? View.VISIBLE : View.INVISIBLE); } }); //Set the click listener for the button to clear the text. The act of clearing the text will hide this button because of the //text listener clearButton.setOnClickListener(new OnClickListener() { #Override public void onClick (final View view) { editField.setText(""); } }); } #Override protected void onLayout (final boolean changed, final int left, final int top, final int right, final int bottom) { super.onLayout(changed, left, top, right, bottom); //Set padding here in the code so the text doesn't run into the close button. This could be done in the XML layout, but then if //the size of the image changes then we constantly need to tweak the padding when the image changes. This way it happens automatically if (!mPaddingSet) { final EditText editField = (EditText) findViewById(R.id.edit_text_field); final ImageButton clearButton = (ImageButton) findViewById(R.id.edit_text_clear); editField.setPadding(editField.getPaddingLeft(), editField.getPaddingTop(), clearButton.getWidth(), editField.getPaddingBottom()); mPaddingSet = true; } } } To make this answer more in line with the question the following steps should be taken: Change the drawable resource to whatever you want... In my case it was a gray X Add a focus change listener to the edit text...
Simply copy paste the following code and it does the trick. editMsg.setOnTouchListener(new View.OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { final int DRAWABLE_LEFT = 0; final int DRAWABLE_TOP = 1; final int DRAWABLE_RIGHT = 2; final int DRAWABLE_BOTTOM = 3; if(event.getAction() == MotionEvent.ACTION_UP) { if(event.getRawX() >= (editMsg.getRight() - editMsg.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { // your action here Toast.makeText(ChatActivity.this, "Message Sent", Toast.LENGTH_SHORT).show(); return true; } } return false; } });
None of the previous solutions worked for me in Xamarin Android. I was able to get the right drawable click listener working using the following: Create the following OnEditTextTouch event listener: private void OnEditTextTouch(object sender, View.TouchEventArgs e) { var rightDrawable = _autoCompleteTextViewSearch.GetCompoundDrawables()[2]; if (rightDrawable == null || e.Event.Action != MotionEventActions.Up) { e.Handled = false; return; } if (e.Event.GetX() >= _autoCompleteTextViewSearch.Width - _autoCompleteTextViewSearch.TotalPaddingRight) { // Invoke your desired action here. e.Handled = true; } // Forward the event along to the sender (crucial for default behaviour) (sender as AutoCompleteTextView)?.OnTouchEvent(e.Event); } Subscribe to the Touch event: _autoCompleteTextViewSearch.Touch += OnEditTextTouch;
I've taked the solution of #AZ_ and converted it in a kotlin extension function: So copy this in your code: #SuppressLint("ClickableViewAccessibility") fun EditText.setDrawableRightTouch(setClickListener: () -> Unit) { this.setOnTouchListener(View.OnTouchListener { _, event -> val DRAWABLE_LEFT = 0 val DRAWABLE_TOP = 1 val DRAWABLE_RIGHT = 2 val DRAWABLE_BOTTOM = 3 if (event.action == MotionEvent.ACTION_UP) { if (event.rawX >= this.right - this.compoundDrawables[DRAWABLE_RIGHT].bounds.width() ) { setClickListener() return#OnTouchListener true } } false }) } You can use it just calling the setDrawableRightTouch function on your EditText: yourEditText.setDrawableRightTouch { //your code }
A probable solution to the above problem could be using android's new material component TextInputLayout. <com.google.android.material.textfield.TextInputLayout android:id="#+id/searchInput" style="#style/Widget.App.TextInputLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="#string/search" app:endIconMode="custom" app:endIconContentDescription="Search" app:endIconDrawable="#drawable/ic_search"> <EditText android:id="#+id/et_search" android:layout_width="match_parent" android:layout_height="match_parent"/> </com.google.android.material.textfield.TextInputLayout> Here the TextInputLayout attribute endIconMode when set, places a button at the end of the enclosed EditText. Additionally app:endIconMode = "custom" allows customization of the icon's click functonality Finally to listen to the end icon clicks call setEndIconClickListener() on the enclosing TextInputLayout component.
#Override public boolean onTouch(View v, MotionEvent event) { Drawable drawableObj = getResources().getDrawable(R.drawable.search_btn); int drawableWidth = drawableObj.getIntrinsicWidth(); int x = (int) event.getX(); int y = (int) event.getY(); if (event != null && event.getAction() == MotionEvent.ACTION_UP) { if (x >= (searchPanel_search.getWidth() - drawableWidth - searchPanel_search.getPaddingRight()) && x <= (searchPanel_search.getWidth() - searchPanel_search.getPaddingRight()) && y >= searchPanel_search.getPaddingTop() && y <= (searchPanel_search.getHeight() - searchPanel_search.getPaddingBottom())) { getSearchData(); } else { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(searchPanel_search, InputMethodManager.SHOW_FORCED); } } return super.onTouchEvent(event); }
and if drawable is on the left, this will help you. (for those work with RTL layout) editComment.setOnTouchListener(new OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { final int DRAWABLE_LEFT = 0; final int DRAWABLE_TOP = 1; final int DRAWABLE_RIGHT = 2; final int DRAWABLE_BOTTOM = 3; if(event.getAction() == MotionEvent.ACTION_UP) { if (event.getRawX() <= (searchbox.getLeft() + searchbox.getCompoundDrawables()[DRAWABLE_LEFT].getBounds().width())) { // your action here return true; } } return false; } });
It is all great but why not to make it really simple? I have faced with that also not so long ago...and android touchlistiner works great but gives limitation in usage..and I came to another solution and I hope that will help you: <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="#+id/zero_row"> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="match_parent"> <ProgressBar android:id="#+id/loadingProgressBar" android:layout_gravity="center" android:layout_width="28dp" android:layout_height="28dp" /> </LinearLayout> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:background="#drawable/edittext_round_corners" android:layout_height="match_parent" android:layout_marginLeft="5dp"> <ImageView android:layout_width="28dp" android:layout_height="28dp" app:srcCompat="#android:drawable/ic_menu_search" android:id="#+id/imageView2" android:layout_weight="0.15" android:layout_gravity="center|right" android:onClick="OnDatabaseSearchEvent" /> <EditText android:minHeight="40dp" android:layout_marginLeft="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#drawable/edittext_round_corners" android:inputType="textPersonName" android:hint="Search.." android:textColorHint="#color/AndroidWhite" android:textColor="#color/AndroidWhite" android:ems="10" android:id="#+id/e_d_search" android:textCursorDrawable="#color/AndroidWhite" android:layout_weight="1" /> <ImageView android:layout_width="28dp" android:layout_height="28dp" app:srcCompat="#drawable/ic_oculi_remove2" android:id="#+id/imageView3" android:layout_gravity="center|left" android:layout_weight="0.15" android:onClick="onSearchEditTextCancel" /> </LinearLayout> <!--android:drawableLeft="#android:drawable/ic_menu_search"--> <!--android:drawableRight="#drawable/ic_oculi_remove2"--> </LinearLayout> </LinearLayout> Now you can create ImageClick listener or event and do what ever you want with text. This edittext_round_corners.xml file <item android:state_pressed="false" android:state_focused="false"> <shape> <gradient android:centerY="0.2" android:startColor="#color/colorAccent" android:centerColor="#color/colorAccent" android:endColor="#color/colorAccent" android:angle="270" /> <stroke android:width="0.7dp" android:color="#color/colorAccent" /> <corners android:radius="5dp" /> </shape> </item>
Better to have ImageButton on Right of edit text and give negative layout margin to overlap with edit text. Set listener on ImageButton and perform operations.
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" > <EditText android:id="#+id/edt_status_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:background="#drawable/txt_box_blank" android:ems="10" android:hint="#string/statusnote" android:paddingLeft="5dp" android:paddingRight="10dp" android:textColor="#android:color/black" /> <Button android:id="#+id/note_del" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_marginRight="1dp" android:layout_marginTop="5dp" android:background="#android:drawable/ic_delete" /> </FrameLayout>
Compound drawables are not supposed to be clickable. It is cleaner to use separate views in a horizontal LinearLayout and use a click handler on them. <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="15dp" android:background="#color/white" android:layout_marginLeft="20dp" android:layout_marginStart="20dp" android:layout_marginRight="20dp" android:layout_marginEnd="20dp" android:layout_gravity="center_horizontal" android:orientation="horizontal" android:translationZ="4dp"> <ImageView android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#color/white" android:minWidth="40dp" android:scaleType="center" app:srcCompat="#drawable/ic_search_map"/> <android.support.design.widget.TextInputEditText android:id="#+id/search_edit" style="#style/EditText.Registration.Map" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:hint="#string/hint_location_search" android:imeOptions="actionSearch" android:inputType="textPostalAddress" android:maxLines="1" android:minHeight="40dp" /> <ImageView android:id="#+id/location_gps_refresh" android:layout_width="wrap_content" android:layout_height="match_parent" android:background="#color/white" android:minWidth="40dp" android:scaleType="center" app:srcCompat="#drawable/selector_ic_gps"/> </LinearLayout>
For anyone who does not want to implement the monstrous click handling. You can achieve the same with a RelativeLayout. With that you even have free handling of the positioning of the drawable. <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.design.widget.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" /> </android.support.design.widget.TextInputLayout> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_centerInParent="true" android:src="#drawable/ic_undo"/> </RelativeLayout> The ImageView position will be the same as you would use drawableEnd - plus you don't need all the touch listener handling. Just a click listener for the ImageView and you are good to go.
This works fro me:) may this help you as well edit_account_name.setOnTouchListener(new View.OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { if (event.getRawX() >= (edit_account_name.getRight())) { //clicked return true; } } return false; } });
I've seen several solutions but I wasn't convinced by any of them. Either very complicated or too simple (non-reusable). This is my favourite approach at the moment: mEditText.setOnTouchListener( new OnEditTextRightDrawableTouchListener(mEditText) { #Override public void OnDrawableClick() { // The right drawable was clicked. Your action goes here. } }); And this is the reusable touch listener: import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.EditText; public abstract class OnEditTextRightDrawableTouchListener implements OnTouchListener { private final EditText mEditText; public OnEditTextRightDrawableTouchListener(#NonNull final EditText editText) { mEditText = editText; } #Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_UP) { final int DRAWABLE_RIGHT_POSITION = 2; final Drawable drawable = mEditText.getCompoundDrawables()[DRAWABLE_RIGHT_POSITION]; if (drawable != null) { final float touchEventX = motionEvent.getX(); final int touchAreaRight = mEditText.getRight(); final int touchAreaLeft = touchAreaRight - drawable.getBounds().width(); if (touchEventX >= touchAreaLeft && touchEventX <= touchAreaRight) { view.performClick(); OnDrawableClick(); } return true; } } return false; } public abstract void OnDrawableClick(); } You can look at the Gist here.
Follow below code for drawable right,left,up,down click: edittextview_confirmpassword.setOnTouchListener(new View.OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { final int DRAWABLE_LEFT = 0; final int DRAWABLE_TOP = 1; final int DRAWABLE_RIGHT = 2; final int DRAWABLE_BOTTOM = 3; if(event.getAction() == MotionEvent.ACTION_UP) { if(event.getRawX() >= (edittextview_confirmpassword.getRight() - edittextview_confirmpassword.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { // your action here edittextview_confirmpassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); return true; } }else{ edittextview_confirmpassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } return false; } }); }
Here's my simple solution, just place ImageButton over EditText: <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="#+id/editTextName" android:layout_width="fill_parent" android:layout_height="wrap_content" android:imeOptions="actionSearch" android:inputType="text"/> <ImageButton android:id="#+id/imageViewSearch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="#drawable/ic_action_search" android:layout_alignParentRight="true" android:layout_centerVertical="true"/> </RelativeLayout>
for left drawable click listener txt.setOnTouchListener(new View.OnTouchListener() { #Override public boolean onTouch(View v, MotionEvent event) { final int DRAWABLE_LEFT = 0; if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getRawX() <= (txt .getCompoundDrawables()[DRAWABLE_LEFT].getBounds().width() + txt.getPaddingLeft() + txt.getLeft())) { //TODO do code here } return true; } } return false; } });
I would like to suggest a way for drawable left! I tried this code and works. txtsearch.setOnTouchListener(new View.OnTouchListener() { #Override public boolean onTouch(View view, MotionEvent event) { final int DRAWABLE_LEFT = 0; int start=txtsearch.getSelectionStart(); int end=txtsearch.getSelectionEnd(); if(event.getAction() == MotionEvent.ACTION_UP) { if(event.getRawX() <= (txtsearch.getLeft() + txtsearch.getCompoundDrawables()[DRAWABLE_LEFT].getBounds().width())) { //Do your action here return true; } } return false; } }); }
I implemented #aristo_sh answer in Mono.Droid (Xamarin), since it's a delegate anonymous method you can't return true or false you have to take take of e.Event.Handled. I am also hiding the keyboard on click editText.Touch += (sender, e) => { e.Handled = false; if (e.Event.Action == MotionEventActions.Up) { if (e.Event.RawX >= (bibEditText.Right - (bibEditText.GetCompoundDrawables()[2]).Bounds.Width())) { SearchRunner(); InputMethodManager manager = (InputMethodManager)GetSystemService(InputMethodService); manager.HideSoftInputFromWindow(editText.WindowToken, 0); e.Handled = true; } } };