19

I try to use padding to increase the touch area of a button. I use

<ImageButton
    android:paddingRight="32dp"
    android:paddingEnd="32dp"
    android:id="@+id/confirm_image_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_centerVertical="true"
    android:background="?selectableItemBackgroundBorderless"
    android:src="?attr/confirmIcon" />

The click area is enlarge. But, selectableItemBackgroundBorderless click effect no longer shown as a perfect circle.

enter image description here


I try to use duplicateParentState technique to overcome.

<FrameLayout
    android:clickable="true"
    android:paddingRight="32dp"
    android:paddingEnd="32dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_centerVertical="true">
    <ImageButton
        android:duplicateParentState="true"
        android:id="@+id/confirm_image_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="?selectableItemBackgroundBorderless"
        android:src="?attr/confirmIcon" />
</FrameLayout>

enter image description here

Now,

  1. The clicked area is enlarged.
  2. The selectableItemBackgroundBorderless circle effect is a perfect circle.

However, it seems to have some weird behavior. When I click on the actual area of the ImageButton, the circle press effect is not shown.

enter image description here

May I know why it is so, and how can I overcome it? I tested using API 26.

Note, I try to avoid using TouchDelegate technique, unless I'm forced to, as it makes our code more complicated.


Additional information

The following is the correct behavior, exhibited by the button for Toolbar.

Ripple effect is shown when the click region is outside the button

enter image description here

Ripple effect is shown when the click region is within the button

enter image description here

However, I have no idea how they implement such behavior.

azizbekian
  • 53,978
  • 11
  • 145
  • 225
Cheok Yan Cheng
  • 49,649
  • 117
  • 410
  • 768

4 Answers4

48

After spending some time I finally found how the "toolbar mystery" works. It's ActionMenuItemView, that is being displayed on the toolbar. And you can see, that inside xml file it has style="?attr/actionButtonStyle" applied to it. ?attr/actionButtonStyle corresponds to Widget.Material.ActionButton and within this style we can see <item name="background">?attr/actionBarItemBackground</item>.

If you want to apply the same effect to your ImageButton, then all you have to do is to apply android:background="?attr/actionBarItemBackground" to it. Thus, having following xml layout:

<ImageButton
    android:id="@+id/confirm_image_button"
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:background="?attr/actionBarItemBackground"
    android:src="@drawable/ic_check_black_24dp" />

You'll receive this output:

enter image description here

Turned on "Show layout bounds" so that actual bounds of ImageButton are visible

If you are curious what ?attr/actionBarItemBackground actually represents, here's it:

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight"
    android:radius="20dp" />

Thus, you can create your drawable and apply it as a background to ImageButton.

azizbekian
  • 53,978
  • 11
  • 145
  • 225
2

The ripples on Android 5.0 start in the center of each view. According to the Material Design rules, the ripples should start where the touch event occurs, so they seem to flow outward from the finger. This was addressed in Android 5.1, but was a bug in Android 5.0

To fix this, you need to use the setHotspot() method, added to Drawable in API Level 21. setHotspot() provides to the drawable a “hot spot”, and RippleDrawable apparently uses this as the emanation point for the ripple effect. setHotspot() takes a pair of float values, presumably with an eye towards using setHotspot() inside of an OnTouchListener, as the MotionEvent reports X/Y positions of the touch event with float values.

Thanks to The Busy Coder's Guide book

You can use this code to correct the ripples bug:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
       btn.setOnTouchListener(new View.OnTouchListener() {
          @TargetApi(Build.VERSION_CODES.LOLLIPOP)
          @Override
          public boolean onTouch(View v, MotionEvent event) {
              v.getBackground()
                .setHotspot(event.getX(), event.getY());
              return(false);
          }
     });
}

other solution:

How to achieve ripple animation using support library?

RippleDrawable


another solution:

in your first try:

I try to use padding to increase the touch area of a button.

you can use this code to put center of ripple at center of ImageButton:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
           btn.setOnTouchListener(new View.OnTouchListener() {
              @TargetApi(Build.VERSION_CODES.LOLLIPOP)
              @Override
              public boolean onTouch(View v, MotionEvent event) {
                  v.getBackground()
                    .setHotspot(v.getWidth()/2f, v.getHeight()/2f);//setting hot spot at center of ImageButton
                  return(false);
              }
         });
    }

maybe another solution when FrameLayout wraps ImageButton:

as you say:

However, it seems to have some weird behavior. When I click on the actual area of the ImageButton, the circle press effect is not shown.

a work around for this problem may be to add frameLayout.performClick() in the onClick() method of ImageButton

or you can simulate a touch on frameLayout like this code when ImageButton is clicked:

// Obtain MotionEvent object
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis() + 100;
float x = 0.0f;// or x = frameLayout.getWidth()/2f
float y = 0.0f;// or y = frameLayout.getHeight()/2f
// List of meta states found here: developer.android.com/reference/android/view/KeyEvent.html#getMetaState()
int metaState = 0;
MotionEvent motionEvent = MotionEvent.obtain(
    downTime, 
    eventTime, 
    MotionEvent.ACTION_UP, 
    x, 
    y, 
    metaState
);

// Dispatch touch event to view
frameLayout.dispatchTouchEvent(motionEvent);
ygngy
  • 2,809
  • 2
  • 12
  • 27
  • May I know the reason it doesn't work without this workaround? – Cheok Yan Cheng Apr 28 '18 at 11:45
  • Do you have some reference source regarding this bug? – Cheok Yan Cheng Apr 28 '18 at 11:46
  • I'm tested using API 26. I can confirm your proposed solution doesn't work. `OnTouchListener` is triggered, but the circle press effect is not shown. – Cheok Yan Cheng Apr 28 '18 at 15:52
  • I had read through those answer but it doesn't answer my doubt. I know how to achieve ripple effect. (As you can see my ripple effect is achieved using technique from https://stackoverflow.com/a/28715666/72437 ) But my problem is different, which is stated in my question clearly. – Cheok Yan Cheng Apr 28 '18 at 18:30
  • @CheokYanCheng at your first try which only has ImageButton with padding you can use `v.getBackground().setHotspot(v.getWidth()/2f, v.getHeight()/2f);` to position center of ripple at center of image button. see my edited answer – ygngy Apr 30 '18 at 14:33
1

I suspect that your problem lies in that you have an ImageButton and not just an ImageView. The default style for the ImageButton is:

<style name="Widget.ImageButton">
    <item name="android:focusable">true</item>
    <item name="android:clickable">true</item>
    <item name="android:scaleType">center</item>
    <item name="android:background">@android:drawable/btn_default</item>
</style>

Replaced was the background, but you still left the item clickable and focusable. In your example, the real active item is the container, so you can either use a regular ImageView, or set the item to be not clickable and focusable manually. Also it is important to set the click listener on the container, not the button/image itself, because it will become clickable.

This is one variant:

<FrameLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingEnd="48dp"
    android:focusable="true"
    android:clickable="true"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    >

    <ImageButton
        android:duplicateParentState="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="?selectableItemBackgroundBorderless"
        android:duplicateParentState="true"
        android:src="@drawable/ic_check_black_24dp"
        android:clickable="false"
        android:focusable="false"
        />
</FrameLayout>

The other one is the same, except the view inside the FrameLayout is:

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="?selectableItemBackgroundBorderless"
    android:duplicateParentState="true"
    android:src="@drawable/ic_check_black_24dp"
    />

Here's the visual proof:

screen capture

This was captured on an emulator running Android 5.0.2. I tried this on my 8.0 phone, works as well.

Community
  • 1
  • 1
Malcolm
  • 38,924
  • 10
  • 67
  • 89
  • I try to replace `ImageButton` with `ImageView`. It still exhibit the same problematic behavior as I described from `ImageButton`. It the pressed region is on the `ImageView`, circle press effect is not shown. – Cheok Yan Cheng Apr 28 '18 at 16:00
  • @CheokYanCheng What about setting focusable and clickable to `false` manually? Also, did you set the `onClickListener` on the image or on the container? – Malcolm Apr 28 '18 at 16:07
  • Yes and yes. I tried https://gist.github.com/yccheok/1657ad18cc5084eb37f7a667ddb3a4b7 , and try to `onClickListener` on container and/or button. (various combinations). It doesn't work. Have you tried on your side? Does it work for you? – Cheok Yan Cheng Apr 28 '18 at 18:15
  • @CheokYanCheng Perfectly. I've updated my answer with the actual code. Maybe something is wrong with your device? – Malcolm Apr 30 '18 at 14:56
1

If you use android:duplicateParentState="true", simply set android:clickable="false" to your ImageButton will make your Button highlight when click on it and its parent

<ImageButton
       ...
       android:duplicateParentState="true"
       android:clickable="false"
  />

Explanation

From the document

When duplication is enabled, this view gets its drawable state from its parent rather than from its own internal properties

=> View change state base on parent state. The default clickable of ImageButton is true => when you click at ImageButton, the state of is parent not change => ImageButton not highlight

=> Set clickable=false will solve the problem

SOME NOTE

1) Don't forget set clickable=true on its parent
2) For TextView, ImageView (or some view which have clickable=false as default), you don't need to set clickable=false
3) BE careful when use setOnClickListener

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

When use setOnClickListener it will also setClickable(true) so in this case you must only set the setOnClickListener for its parent layout

Linh
  • 43,513
  • 18
  • 206
  • 227