Typically how sticky headers work is that there's some sort of scrollable data that is divided into sections, each with their own header, and as you scroll down, the headers of subsequent sections replace the header at the top of the ScrollView.
What I need is to have additional sticky headers within each respective section. For example, if header1 is stuck to the top, its first section's header --header1a-- is stuck underneath it, but when we get to section 1b, 1b's header will replace 1a's, but leaving header1 stuck in the same place; and when we finally get to section 2, header2 will replace the currently stuck headers from the previous section -- header1 and header1b.
Here is a ScrollView implementation that implements sticky headers in a one-dimensional fashion:
https://github.com/emilsjolander/StickyScrollViewItems
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import java.util.ArrayList;
/**
*
* #author Emil Sj�lander - sjolander.emil#gmail.com
*
*/
public class StickyScrollView extends ScrollView {
/**
* Tag for views that should stick and have constant drawing. e.g. TextViews, ImageViews etc
*/
public static final String STICKY_TAG = "sticky";
/**
* Flag for views that should stick and have non-constant drawing. e.g. Buttons, ProgressBars etc
*/
public static final String FLAG_NONCONSTANT = "-nonconstant";
/**
* Flag for views that have aren't fully opaque
*/
public static final String FLAG_HASTRANSPARANCY = "-hastransparancy";
/**
* Default height of the shadow peeking out below the stuck view.
*/
private static final int DEFAULT_SHADOW_HEIGHT = 10; // dp;
private ArrayList<View> mStickyViews;
private View mCurrentlyStickingView;
private float mStickyViewTopOffset;
private int mStickyViewLeftOffset;
private boolean mRedirectTouchesToStickyView;
private boolean mClippingToPadding;
private boolean mClipToPaddingHasBeenSet;
private int mShadowHeight;
private Drawable mShadowDrawable;
private final Runnable mInvalidateRunnable = new Runnable() {
#Override
public void run() {
if(mCurrentlyStickingView !=null){
int l = getLeftForViewRelativeOnlyChild(mCurrentlyStickingView);
int t = getBottomForViewRelativeOnlyChild(mCurrentlyStickingView);
int r = getRightForViewRelativeOnlyChild(mCurrentlyStickingView);
int b = (int) (getScrollY() + (mCurrentlyStickingView.getHeight() + mStickyViewTopOffset));
invalidate(l,t,r,b);
}
postDelayed(this, 16);
}
};
public StickyScrollView(Context context) {
this(context, null);
}
public StickyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.scrollViewStyle);
}
public StickyScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setup();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.StickyScrollView, defStyle, 0);
final float density = context.getResources().getDisplayMetrics().density;
int defaultShadowHeightInPix = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
mShadowHeight = a.getDimensionPixelSize(
R.styleable.StickyScrollView_stuckShadowHeight,
defaultShadowHeightInPix);
int shadowDrawableRes = a.getResourceId(
R.styleable.StickyScrollView_stuckShadowDrawable, -1);
if (shadowDrawableRes != -1) {
mShadowDrawable = context.getResources().getDrawable(
shadowDrawableRes);
}
a.recycle();
}
/**
* Sets the height of the shadow drawable in pixels.
*
* #param height
*/
public void setShadowHeight(int height) {
mShadowHeight = height;
}
public void setup(){
mStickyViews = new ArrayList<View>();
}
private int getLeftForViewRelativeOnlyChild(View v){
int left = v.getLeft();
while(v.getParent() != getChildAt(0)){
v = (View) v.getParent();
left += v.getLeft();
}
return left;
}
private int getTopForViewRelativeOnlyChild(View v){
int top = v.getTop();
while(v.getParent() != getChildAt(0)){
v = (View) v.getParent();
top += v.getTop();
}
return top;
}
private int getRightForViewRelativeOnlyChild(View v){
int right = v.getRight();
while(v.getParent() != getChildAt(0)){
v = (View) v.getParent();
right += v.getRight();
}
return right;
}
private int getBottomForViewRelativeOnlyChild(View v){
int bottom = v.getBottom();
while(v.getParent() != getChildAt(0)){
v = (View) v.getParent();
bottom += v.getBottom();
}
return bottom;
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(!mClipToPaddingHasBeenSet){
mClippingToPadding = true;
}
notifyHierarchyChanged();
}
#Override
public void setClipToPadding(boolean clipToPadding) {
super.setClipToPadding(clipToPadding);
mClippingToPadding = clipToPadding;
mClipToPaddingHasBeenSet = true;
}
#Override
public void addView(View child) {
super.addView(child);
findStickyViews(child);
}
#Override
public void addView(View child, int index) {
super.addView(child, index);
findStickyViews(child);
}
#Override
public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
super.addView(child, index, params);
findStickyViews(child);
}
#Override
public void addView(View child, int width, int height) {
super.addView(child, width, height);
findStickyViews(child);
}
#Override
public void addView(View child, android.view.ViewGroup.LayoutParams params) {
super.addView(child, params);
findStickyViews(child);
}
#Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if(mCurrentlyStickingView != null){
canvas.save();
canvas.translate(getPaddingLeft() + mStickyViewLeftOffset, getScrollY() + mStickyViewTopOffset + (mClippingToPadding ? getPaddingTop() : 0));
canvas.clipRect(0, (mClippingToPadding ? -mStickyViewTopOffset : 0), getWidth() - mStickyViewLeftOffset,mCurrentlyStickingView.getHeight() + mShadowHeight + 1);
if (mShadowDrawable != null) {
int left = 0;
int right = mCurrentlyStickingView.getWidth();
int top = mCurrentlyStickingView.getHeight();
int bottom = mCurrentlyStickingView.getHeight() + mShadowHeight;
mShadowDrawable.setBounds(left, top, right, bottom);
mShadowDrawable.draw(canvas);
}
canvas.clipRect(0, (mClippingToPadding ? -mStickyViewTopOffset : 0), getWidth(), mCurrentlyStickingView.getHeight());
if(getStringTagForView(mCurrentlyStickingView).contains(FLAG_HASTRANSPARANCY)){
showView(mCurrentlyStickingView);
mCurrentlyStickingView.draw(canvas);
hideView(mCurrentlyStickingView);
}else{
mCurrentlyStickingView.draw(canvas);
}
canvas.restore();
}
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(ev.getAction()==MotionEvent.ACTION_DOWN){
mRedirectTouchesToStickyView = true;
}
if(mRedirectTouchesToStickyView){
mRedirectTouchesToStickyView = mCurrentlyStickingView != null;
if(mRedirectTouchesToStickyView){
mRedirectTouchesToStickyView =
ev.getY()<=(mCurrentlyStickingView.getHeight()+ mStickyViewTopOffset) &&
ev.getX() >= getLeftForViewRelativeOnlyChild(mCurrentlyStickingView) &&
ev.getX() <= getRightForViewRelativeOnlyChild(mCurrentlyStickingView);
}
}else if(mCurrentlyStickingView == null){
mRedirectTouchesToStickyView = false;
}
if(mRedirectTouchesToStickyView){
ev.offsetLocation(0, -1*((getScrollY() + mStickyViewTopOffset) - getTopForViewRelativeOnlyChild(mCurrentlyStickingView)));
}
return super.dispatchTouchEvent(ev);
}
private boolean hasNotDoneActionDown = true;
#Override
public boolean onTouchEvent(MotionEvent ev) {
if(mRedirectTouchesToStickyView){
ev.offsetLocation(0, ((getScrollY() + mStickyViewTopOffset) - getTopForViewRelativeOnlyChild(mCurrentlyStickingView)));
}
if(ev.getAction()==MotionEvent.ACTION_DOWN){
hasNotDoneActionDown = false;
}
if(hasNotDoneActionDown){
MotionEvent down = MotionEvent.obtain(ev);
down.setAction(MotionEvent.ACTION_DOWN);
super.onTouchEvent(down);
hasNotDoneActionDown = false;
}
if(ev.getAction()==MotionEvent.ACTION_UP || ev.getAction()==MotionEvent.ACTION_CANCEL){
hasNotDoneActionDown = true;
}
return super.onTouchEvent(ev);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
doTheStickyThing();
}
private void doTheStickyThing() {
View viewThatShouldStick = null;
View approachingStickyView = null;
for(View v : mStickyViews){
int viewTop = getTopForViewRelativeOnlyChild(v) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop());
if(viewTop<=0){
if(viewThatShouldStick==null || viewTop>(getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()))){
viewThatShouldStick = v;
}
}else{
if(approachingStickyView == null || viewTop<(getTopForViewRelativeOnlyChild(approachingStickyView) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()))){
approachingStickyView = v;
}
}
}
if(viewThatShouldStick!=null){
mStickyViewTopOffset = approachingStickyView == null ? 0 : Math.min(0, getTopForViewRelativeOnlyChild(approachingStickyView) - getScrollY() + (mClippingToPadding ? 0 : getPaddingTop()) - viewThatShouldStick.getHeight());
if(viewThatShouldStick != mCurrentlyStickingView){
if(mCurrentlyStickingView !=null){
stopStickingCurrentlyStickingView();
}
// only compute the left offset when we start sticking.
mStickyViewLeftOffset = getLeftForViewRelativeOnlyChild(viewThatShouldStick);
startStickingView(viewThatShouldStick);
}
}else if(mCurrentlyStickingView !=null){
stopStickingCurrentlyStickingView();
}
}
private void startStickingView(View viewThatShouldStick) {
mCurrentlyStickingView = viewThatShouldStick;
if(getStringTagForView(mCurrentlyStickingView).contains(FLAG_HASTRANSPARANCY)){
hideView(mCurrentlyStickingView);
}
if(((String) mCurrentlyStickingView.getTag()).contains(FLAG_NONCONSTANT)){
post(mInvalidateRunnable);
}
}
private void stopStickingCurrentlyStickingView() {
if(getStringTagForView(mCurrentlyStickingView).contains(FLAG_HASTRANSPARANCY)){
showView(mCurrentlyStickingView);
}
mCurrentlyStickingView = null;
removeCallbacks(mInvalidateRunnable);
}
/**
* Notify that the sticky attribute has been added or removed from one or more views in the View hierarchy
*/
public void notifyStickyAttributeChanged(){
notifyHierarchyChanged();
}
private void notifyHierarchyChanged(){
if(mCurrentlyStickingView !=null){
stopStickingCurrentlyStickingView();
}
mStickyViews.clear();
findStickyViews(getChildAt(0));
doTheStickyThing();
invalidate();
}
private void findStickyViews(View v) {
if(v instanceof ViewGroup){
ViewGroup vg = (ViewGroup)v;
for(int i = 0 ; i<vg.getChildCount() ; i++){
String tag = getStringTagForView(vg.getChildAt(i));
if(tag!=null && tag.contains(STICKY_TAG)){
mStickyViews.add(vg.getChildAt(i));
}else if(vg.getChildAt(i) instanceof ViewGroup){
findStickyViews(vg.getChildAt(i));
}
}
}else{
String tag = (String) v.getTag();
if(tag!=null && tag.contains(STICKY_TAG)){
mStickyViews.add(v);
}
}
}
private String getStringTagForView(View v){
Object tagObject = v.getTag();
return String.valueOf(tagObject);
}
private void hideView(View v) {
if(Build.VERSION.SDK_INT>=11){
v.setAlpha(0);
}else{
AlphaAnimation anim = new AlphaAnimation(1, 0);
anim.setDuration(0);
anim.setFillAfter(true);
v.startAnimation(anim);
}
}
private void showView(View v) {
if(Build.VERSION.SDK_INT>=11){
v.setAlpha(1);
}else{
AlphaAnimation anim = new AlphaAnimation(0, 1);
anim.setDuration(0);
anim.setFillAfter(true);
v.startAnimation(anim);
}
}
}
What I'm trying to do is to adapt it to suit my needs, but I've tried poking around in this implementation to see how it does what it does and I cannot figure out how it gets the view to get stuck to the top of the ScrollView. Does anyone have any idea how this works?
Edit:
Here is the layout that I want to apply this concept too:
*Keep in mind that the Headers (Headers 1 & 2) are custom ViewGroups that contains the Sub-Headers (Header 1a, 1b, 2a); which are also custom ViewGroups that contain custom views which are the Items.
The StickyScrollView you are using is just saving a tag to whether it should be sticky or not and if not which child of scrollview is it's header, and according to that it is maintaining it as a first child view.
If you want to use this StickyScrollView only you have to modify it and maintain one more tag as sub-header.
I will suggest rather using this ScrollView, you can use this ListView. It is very easy to implement and it works.
You can use header-decor for your requirement. Internally its using RecyclerView, so it is advisable to use it. Check Double Header section in below gif.
Hope this will help you.
This isn't rocket science. There's two key parts to understanding this.
First is in the method doTheStickyThing. This figures out what goes where.
The initial step is figuring out which header to stick. Once you scroll down, you have views both above and below the top of the scroll view. You want to stick the bottom-most header that is still above the top of the scroll view. So you see a lot of expressions like this:
getTopForViewRelativeOnlyChild(viewThatShouldStick) - getScrollY() + (clippingToPadding ? 0 : getPaddingTop()))
That resulting value is just the offset of the top of the view from the top of the scroll view. If the header is above the top of the scroll view, the value is negative. So it turns out you want the header that has the greatest offset value that is still less than or equal to zero. The winning view gets assigned to viewThatShouldStick.
Now that you have a sticking header, you want to know which following header might start pushing that one out of the way when scrolling. That gets assigned to approachingView
If the approaching view is pushing the header out of the way, you have to offset the top of the header. That value is assigned to stickyViewTopOffset
The second key part is drawing the header. That's done in dispatchDraw.
Here's the trick to making the view look "stuck": The normal rendering logic would like to put that header at a certain place based on its current bounds. We can just move the canvas (translate) underneath that header so that it draws at the top of the scroll view instead of wherever it would normally draw. Then we tell the view to draw itself. This happens after all the list item views have been already been drawn, so the header appears to float on top of the list items.
When we move the canvas around, we also have to take into account the case where another approaching header is starting to push this one out of the way. The clipping handles some corner cases concerning how things should look when paddings are involved.
I started working on modifying the code to do what you wanted, but things got complicated fast.
Instead of tracking two headers, you need to track three headers: header, subheader, and approaching header. Now you have to handle the top offset of the subheader along with the top offset of the header. And then you have two scenarios: First is that the approaching header is a main header. This is going to modify both top offsets. But when the approaching header is a subheader, only the top offset of the pinned subheader is affected, and the main header offset stays the same.
I can get this, but I'm short on time right now. I'll finish off the code and post it if I can find the time.
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;
}
}
};