This looks easy, but I'm not able to disable an ImageButton. It continues to receive click events, and its appearance don't change like a standard Button would.
There are some similar questions on SO, but they don't help me.
Even with a very simple layout like this :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<ImageButton
android:id="#+id/btn_call"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:clickable="false"
android:enabled="false"
android:src="#android:drawable/sym_action_call" />
</LinearLayout>
The button is still enabled and I can click it.
What's strange is that if I change the ImageButton to a simple Button, then it works as expected. The button becomes disabled and unclickable. I don't understand. Does anyone have an idea?
Here's the code I use to disable an ImageButton and make it look grayed out:
/**
* Sets the specified image buttonto the given state, while modifying or
* "graying-out" the icon as well
*
* #param enabled The state of the menu item
* #param item The menu item to modify
* #param iconResId The icon ID
*/
public static void setImageButtonEnabled(Context ctxt, boolean enabled, ImageButton item,
int iconResId) {
item.setEnabled(enabled);
Drawable originalIcon = ctxt.getResources().getDrawable(iconResId);
Drawable icon = enabled ? originalIcon : convertDrawableToGrayScale(originalIcon);
item.setImageDrawable(icon);
}
/**
* Mutates and applies a filter that converts the given drawable to a Gray
* image. This method may be used to simulate the color of disable icons in
* Honeycomb's ActionBar.
*
* #return a mutated version of the given drawable with a color filter
* applied.
*/
public static Drawable convertDrawableToGrayScale(Drawable drawable) {
if (drawable == null) {
return null;
}
Drawable res = drawable.mutate();
res.setColorFilter(Color.GRAY, Mode.SRC_IN);
return res;
}
Simply call setImageButtonEnabled(); the only downside is you need the image's resource ID in here because it's not possible to revert a transformed icon back into the original.
ImageButton has different inheritance chain meaning it does not extend Button:
ImageButton < ImageView < View
It continues to receive click events
Here is what happens when you set a click listener for the View:
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
So if you set a listener the android:clickable="false" changes to android:clickable="true".
and its appearance don't change like a standard Button would
You should supply a drawable state list to the view so it could set an appropriate image based on android:enabled. Do you have this? Or you have the only image for your button?
EDIT: You can find info on StateListDrawable here. android:state_enabled is what you need to use in the list in order to tell the OS what image to use for that state.
EDIT2: Since you really need to add a listener you can make a check inside of the listener if (!isEnabled()) { return; } else { /* process the event */ }.
if you want to disable an image button,on click event, set the the property "setEnabled" to false
Ex: imgButton.setEnabled(false);
Make sure there is no view with same id in your view hierarchy and you do not add any click listener to that view.
Taking advantage of the Oleg Vaskevich's answer. Can be made an answer for Kotlin.
Make a Extension Function for ImageButton, this way:
/**
* Sets the specified image buttonto the given state, while modifying or
* "graying-out" the icon as well
*
* #param enabled The state of the menu item
* #param iconResId The icon ID
*/
fun ImageButton.setButtonEnabled(enabled: Boolean, iconResId: Int) {
isEnabled = enabled
val originalIcon = context.resources.getDrawable(iconResId)
val icon = if (enabled) originalIcon else convertDrawableToGrayScale(originalIcon)
setImageDrawable(icon)
}
And you get a little less reliant on providing Context
I managed to build a solution inspired by Oleg Vaskevich's answer, but without the need to pass drawable resource ID to setEnabled().
Here is Kotlin code, inside of utility module:
fun Drawable.deepCopy(): Drawable =
constantState?.newDrawable()?.mutate() ?:
throw RuntimeException("Called on null Drawable!")
fun Drawable.toGrayscale(): Drawable =
deepCopy().apply { setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN) }
fun ImageButton.setAndShowEnabled(enabled: Boolean) {
if (enabled == isEnabled)
return
isEnabled = enabled
if (enabled) {
setImageDrawable(tag as Drawable)
}
else {
if (tag == null)
tag = drawable
setImageDrawable(drawable.toGrayscale())
}
}
It can be used like this:
val button: ImageButton = findViewById(...)
// ...
button.setAndShowEnabled(false)
// launch async operation
GlobalScope.launch {
// do work here
// unblock button
button.setAndShowEnabled(true)
}
As other answers have said, you cannot disable an ImageButton in the layout XML as you can a Button, but you can disable both the same way at runtime:
In Java:
button.setEnabled(false); // setEnabled(boolean) on TextView
imgButton.setEnabled(false); // setEnabled(boolean) on View
In both cases the button is disabled -- no click events get to its onClickListener.
You can also change the icon color of the disabled ImageButton the same way you change the text color on a disabled Button, assuming the icon is tintable.
In the layout XML:
<Button
...
android:textColor="#drawable/button_color_selector" />
<ImageButton
...
android:tint="#drawable/button_color_selector" />
Now setEnable(boolean) on the Button or ImageButton changes the text or icon color according to the states in your button_color_selector.xml
To improve on Ivan's 2018 answer: This is a much simpler method (Kotlin):
fun ImageButton.setAndShowEnabled(enabled: Boolean) {
val filter = PorterDuffColorFilter(GRAY, PorterDuff.Mode.SCREEN)
if (enabled == this.isEnabled)
return
this.isEnabled = enabled
if (enabled) {
drawable.colorFilter = null // Removes the filter.
} else {
drawable.mutate() // Repeated calls are no-ops.
drawable.colorFilter = filter
}
}
Related
Context of the Problem
I was currently trying to build a functionality to fast forward videos in ExoPlayer exactly like how Youtube does it. I thought that I could add a double click listener and I was able to do that using the answer given here.
Problem
The problem I am facing is that I was getting the ripple animation even when I do a single click which is not what I want. I want to be able to get a ripple only when a double click is triggered. So, for this, I tried to get the number of clicks and then accordingly set the ripple visibility on the view but it did not seem to work for me and instead giving a weird behaviour. The issue can be seen in the gif given below* :
It can be clearly seen that on single click, it shows ripple. On double click, it shows one ripple right but the other one comes out from nowhere!
Code
Kotlin
var doubleClickLastTime = 0L
var clicksDone = 0
binding.forward.setOnClickListener {
if((System.currentTimeMillis() - doubleClickLastTimeF) < 350){
doubleClickLastTime = 0
clicksDone = 0
exoPlayer.seekTo(exoPlayer.contentPosition + 10000)
}else{
if (clicksDoneF == 0){
binding.forward.foreground = null
}else{
binding.forward.foreground = with(TypedValue()) {
this#VideoPlayer.theme.resolveAttribute(
android.R.attr.selectableItemBackground, this, true)
ContextCompat.getDrawable(this#VideoPlayerFromUrl, resourceId)
}
}
clicksDone++
doubleClickLastTime = System.currentTimeMillis()
}
}
XML
<androidx.cardview.widget.CardView
android:id="#+id/forward"
android:layout_width="550dp"
android:layout_height="match_parent"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginVertical="50dp"
android:foreground="?selectableItemBackgroundBorderless"
app:cardCornerRadius="16px"
app:cardElevation="0dp"
android:backgroundTint="#android:color/transparent"/>
*Sorry for the bad gif quality
If you convert it in kotlin you can use this
GestureDetector gestureDetector=new GestureDetector(getApplicationContext(),new GestureDetector.SimpleOnGestureListener(){
#Override
public boolean onDoubleTap(MotionEvent e) {
// you can put your code here
return super.onDoubleTap(e);
}
});
I have a view. The view might become visible at some time in the future. When this view is visible I want to call a method. How to do this?
val editText = findViewById<EditText>(R.id.editText)
// editText might become invisible in some time in future
// and in some in future it might visible
if(editText.isVisible(){
// code to be executed
}
Code for View.isVisible() :
fun View.isVisible() = this.visibility == View.VISIBLE // check if view is visible
Is there anything like View.setOnClickListener which could be applied and triggered when the view is visible-
editText.setOnClickListener { view ->
}
click listener is callback when the view is being clicked. it has no concern with its visibility. There is no method like isVisible(). to check Visibility
if(yourView.getVisiblity()==View.VISIBLE){ //your task}
for kotlin:
if(youView.visibility==View.VISIBLE){//your task}
I might initialize a variable int status of visibility and set it to 0 with the view invisible.
Now I would create a function instead directly setting the visibility of the view.
For example a function named onVisibilityChanged();
In that function add the set visibility code followed by setting the int to 0 or 1 as per the visibility an if-else block.
If you just set the view to visible, set the int to 1.
The reason for adding if-else block is to configure your actions based on the visibility status.
So that gives you the freedom to do whatever you want bases on the visibility.
Make sure you add this code in such a way that it is executed anytime you want.
Use the performClick() function to click any button or any other view.
I hope you understand. Or comment any query. I would have posted the code for the same but it looks like you are using Kotlin. So I'll try to post it in Kotlin if possible.
The main intention of doing such a thing is when the value of int changes, the app knows what to do and also knows that visibility has changed.
Just call the function wherever you want. It will be easy.
So this is what I am trying to do:
int visibilityStatus;
textview = findViewById(R.id.textview);
getInitialVisibility();
funcOnVisibilityChange();
}
private void getCurrentVisibility() {
if (textview.getVisibility() == View.VISIBLE) {
visibilityStatus = 1;
} else {
visibilityStatus = 0;
}
}
private void funcOnVisibilityChange() {
//Now change the visibility in thia function
textview.setVisibility(View.VISIBLE);
int currentVisibilityStatus;
if (textview.getVisibility() == View.VISIBLE) {
currentVisibilityStatus = 1;
} else {
currentVisibilityStatus = 0;
}
if (visibilityStatus != currentVisibilityStatus) {
//Visibility status has changed. Do your task
else {
//visibility status not changed
}
}
}
So all that we are doing is getting visibility of the view when the app is started and when you somehow change its visibility. Here in the example, I've directly changed the visibility. So wherever you know that visibility is going to change just put the funcOnVisibilityChange() and it will do your job... hope it helps. Let me know if you need more clarification.
I'm trying to play around with some Kotlin and Anko (more familiar with iOS) and taking from their example, there is this code:
internal open class TextListWithCheckboxItem(val text: String = "") : ListItem {
protected inline fun createTextView(ui: AnkoContext<ListItemAdapter>, init: TextView.() -> Unit) = ui.apply {
textView {
id = android.R.id.text1
text = "Text list item" // default text (for the preview)
isClickable = true
setOnClickListener {
Log.d("test", "message")
}
init()
}
checkBox {
id = View.generateViewId()
setOnClickListener {
Log.d("hi", "bye")
}
init()
}
}.view
My row appears how I want with a checkbox and textview. But I want to bind an action to the row selection not the checkbox selection. Putting a log message in both, I see that I get a log message when the row is selected which flips the checkbox. It does not, however, log my "test:message" from the textView click handler. Is there a way to get around this?
Apparently your issue has been addressed here. As the checkbox is consuming all the focus of ListItem you should set the CheckBox's focusable flag to false:
checkBox {
focusable = View.NOT_FOCUSABLE
}
Unfortunately setFocusable call requires at least API 26, but you could define view .xml and inflate the view manually as described here:
<CheckBox
...
android:focusable="false" />
Alternatively you could try setting a onTouchListener returning false which means the touch event will be passed to underlying views.
Let me know if it works ;)
This question is similar to this. However it did not solve my problem.
I have a ToggleButton and when a user clicks, I do not want to change the state of the ToggleButton, as I am programatically changing the state from another Activity.
How do I override it?
Here is my Activity code:
<ToggleButton
android:id="#+id/alarm1"
android:background="#drawable/check"
android:layout_alignParentLeft="true"
android:layout_margin="8dp"
android:textOn=""
android:onClick="alarmSet1"
android:textOff=""
android:focusable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
Here is the Java code:
public void alarmSet1(View view)
{
int a1=1;
int idTime = (int) System.currentTimeMillis();
Intent intent = new Intent(MainActivity.this, AddAlarm.class);
intent.putExtra("pendInt",idTime);
intent.putExtra("tts",a1);
startActivity(intent);
}
I am late to the party, but I had the exact same question and the given answer didn't fit my needs, so I made something myself.
What I wanted (correct me if this was not the original question):
I want to intercept clicks/swipes on a Switch, RadioButton, CheckBox or any other CompoundButton, making sure the button itself doesn't toggle state, but instead calling a function which will eventually lead to the correct state being set programatically. (eg. using LiveData) so the state of the button is always the same as the state of the data it toggles.
What I made:
/**
* Use this if you want your compoundButton to do something when (un) checked,
* but not change it's state by itself.
*
* It will block the actual changing of the button, but instead run [onCheckedChanged]
* which should in turn eventually lead to setting `isChecked` to get proper setting of the switch
* after its action has been performed
* This might cause some trouble as it will trigger on programmatic sets outside of this listener.
* To get around that, use [setIsCheckedWithoutBypassedListener] if you don't want that to happen.
*/
fun CompoundButton.setInterceptedOnCheckedChangedListener(onCheckedChanged: CompoundButton.OnCheckedChangeListener){
compoundButtonListeners[this] = onCheckedChanged
setOnCheckedChangeListener { compoundButton, b ->
compoundButton.isChecked = !compoundButton.isChecked
onCheckedChanged.onCheckedChanged(compoundButton, b)
}
}
/**
* Set value without triggering the listener added in [setInterceptedOnCheckedChangedListener]
*/
fun CompoundButton.setIsCheckedWithoutBypassedListener(isChecked: Boolean){
compoundButtonListeners[this]?.let{ l ->
setOnCheckedChangeListener(null)
this.isChecked = isChecked
setOnCheckedChangeListener(l)
}
}
/**
* Holds the OnCheckedChangeListeners for the compoundbuttons, as they are private and cannot
* be retreived otherwise.
*/
private val compoundButtonListeners = WeakHashMap<CompoundButton, CompoundButton.OnCheckedChangeListener>()
If this ends up putting a lot of _, _ ->'s in your code, feel free to replace the CompoundButton.OnCheckedChangeListener with a View.OnClickListener.
Leaving this here in case somebody else has the same question as I had.
I am pretty sure using a WeakHashMap prevents a memory leak that would happen on recreating activity. If this is not the case, please educate me :)
It is simple:
Create a CompoundButton.OnCheckedChangeListener and override onCheckedChanged like this:
private CompoundButton.OnCheckedChangeListener _toggleButtonListener = new CompoundButton.OnCheckedChangeListener()
{
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
compoundButton.setChecked(false);
// And now, do whatever you want.
}
};
Of course, you have to attach the listener to your button:
... somewhere in the onCreate() method:
ToggleButton toggleButton=findViewById(R.id.alarm1);
toggleButton.setOnCheckedChangeListener(_toggleButtonListener);
And thats it.
Keep coding, and let the code be with you!
The way app works is the following: App prompts 30 buttons to user and user may guess the right ones by tapping. When user taps some button all the buttons (say a view containing these buttons) should be locked while corresponding (right or wrong guess) animation is playing. Tapped button by itself should be disabled till the next round. After animation is finished all not tapped previously buttons (say a view containing these buttons) should be available again.
So I have a Layout which includes another layout with these 30 buttons:
...
<RelativeLayout
android:id="#+id/alphabetContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="#layout/alphabet" />
</RelativeLayout>
...
Now I need to lock the buttons from being clicked and then unlock. So I tried:
...
private RelativeLayout alphabetPanel;
...
public void onCreate(){
...
alphabetPanel = (RelativeLayout) findViewById(R.id.alphabetContainer);
...
}
...
private void lockButtons(){
alphabetPanel.setEnabled(false);
}
but this doesn't lock buttons. I also tried:
alphabetPanel.setFocusable(false);
alphabetPanel.setClickable(false);
Doesn't help either. Seems like it all relies only to a layout by itself but not the views it contains.
Also I tried to add a fake layout to place it over layout with buttons by bringing it to the front. This is a workaround and its tricky cuz both layouts must be placed inside a RelativeLayout only:
...
blockingLayout = new RelativeLayout(this);
blockingLayout.setLayoutParams(alphabetPanel.getLayoutParams());
...
but this works very strange: somehow both layouts in this case appears and disappears every second or so or doesn't appear at all - I cant understand that at all cuz there is no setVisibility() method used in code!
The only one way left is to iterate every view (button) to make it disabled and than back.
Is there any other way?
UPDATE
Finally I had to add a "wall"-layout into the xml. Now by making it clickable and focusable it becomes a solution.
Try setting for each Button's xml definition
android:duplicateParentState="true"
I'm not sure, but I think it should make them not only to seem disabled, but also to act accordingly.
Hmm it surprises me that disabling the parent-layout doesn't work.. as far as i know it should.
Try fetching your included layout instead, and disable that.
Anyway, if all else fails you can always loop through the buttons themselves.
for(int i=0;i<relativeLayout.getChildCount();i++){
View child=relativeLayout.getChildAt(i);
//your processing....
child.setEnabled(false);
}
I used extension to lock and unlock the view
//lock
fun View.lock() {
isEnabled = false
isClickable = false}
//unlock
fun View.unlock() {
isEnabled = true
isClickable = true}
if you want to lock all children of the view group
//lock children of the view group
fun ViewGroup.lockAllChildren() {
views().forEach { it.lock() }}
//unlock children of the view group
fun ViewGroup.unlockAllChildren() {
views().forEach { it.unlock() }}
firstly define your button
Button bit = (Button)findViewById(R.id.but);
bit.setEnabled(false);
and set enabled false;
Java:-
public void disableButtons(Layout layout) {
// Get all touchable views
ArrayList<View> layoutButtons = layout.getTouchables();
// loop through them, if they are instances of Button, disable them.
for(View v : layoutButtons){
if( v instanceof Button ) {
((Button)v).setEnabled(false);
}
}
}
Kotlin:-
fun disableButtons(layout: Layout) {
// Get all touchable views
val layoutButtons: ArrayList<View> = layout.getTouchables()
// loop through them, if they are instances of Button, disable them.
for (v in layoutButtons) {
if (v is Button) {
(v as Button).setEnabled(false)
}
}
}
Retrieve all touchables views into an ArrayList, then loop through them and check if it is an instance of the Button or TextView or which ever you want, then disable it!
In case data binding is needed
import android.view.ViewGroup
import android.widget.Button
import androidx.core.view.children
import androidx.databinding.BindingAdapter
#BindingAdapter("disableButtons")
fun ViewGroup.setDisableButtons(disableButtons: Boolean) {
children.forEach {
(it as? Button)?.isEnabled = !disableButtons
}
}
Usage:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="#dimen/guideline"
app:disableButtons="#{vm.busy}">
....
</androidx.constraintlayout.widget.ConstraintLayout>
Might work in constraint layout . Use group widget and add all the button ids.
In the java code set enabled false for the group.
For disable all buttons in any nested layouts.
void DisableAllButtons( ViewGroup viewGroup ){
for( int i = 0; i < viewGroup.getChildCount(); i++ ){
if( viewGroup.getChildAt(i) instanceof ViewGroup ){
DisableAllButtons( (ViewGroup) viewGroup.getChildAt(i) );
}else if( viewGroup.getChildAt(i) instanceof Button ){
viewGroup.getChildAt(i).setEnabled( false );
}
}
}
write these two lines on your button declartion in XML
android:setEnabled="false"
android:clickable="false"