30

Is there a way to handle a view visibility change (say, from GONE to VISIBLE) without overriding the view?

Something like View.setOnVisibilityChangeListener();?

Jean-François Corbett
  • 37,420
  • 30
  • 139
  • 188
Federico Ponzi
  • 2,682
  • 4
  • 34
  • 60
  • I don't know for sure, but I'd say there isn't such a thing, as it would put a lot of work onto the whole system to permanently track the visibility of all views all the time, in order to notify a possible listener. – Ridcully Sep 25 '15 at 08:10

6 Answers6

72

You can use a GlobalLayoutListener to determine if there are any changes in the views visibility.

myView.setTag(myView.getVisibility());
myView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        int newVis = myView.getVisibility();
        if((int)myView.getTag() != newVis)
        {
            myView.setTag(myView.getVisibility());
            //visibility has changed
        }
    }
});
string.Empty
  • 10,393
  • 4
  • 39
  • 67
  • 4
    This works but it just works for layout changes respectively for changes from GONE to VISIBLE and vice versa because INVISIBLE does not trigger a layout change. Am I right? – ToBe Nov 16 '17 at 10:13
  • @ToBe in this line myView.setTag(myView.getVisibility()); you can set whatever you want to handle afterwards – Agna JirKon Rx Feb 23 '18 at 17:13
6

In addition to @string.Empty's solution, this is an extension implementation for Kotlin.

fun View.visibilityChanged(action: (View) -> Unit) {
    this.viewTreeObserver.addOnGlobalLayoutListener {
        val newVis: Int = this.visibility
        if (this.tag as Int? != newVis) {
            this.tag = this.visibility

            // visibility has changed
            action(this)
        }
    }
}

Implement it like this

myView.visibilityChanged { view ->
    when (view.visibility) {
        VISIBLE -> { /* Do something here */ }
        GONE -> { /* or here */ }
    }

}
DevinM
  • 1,112
  • 1
  • 12
  • 29
  • The OnGlobalLayoutListener triggers when the layout changes. So if you do not check for the change in visibility within the listener then it will trigger unexpectedly. – string.Empty Mar 29 '22 at 15:20
  • 1
    @string.Empty I updated the implementation for kotlin and used yours with tagging. – DevinM Aug 16 '22 at 17:49
3

Instead of subclassing you can use decoration:

class WatchedView {

    static class Listener {
        void onVisibilityChanged(int visibility);
    }

    private View v;
    private Listener listener;

    WatchedView(View v) {
        this.v = v;
    }

    void setListener(Listener l) {
        this.listener = l;
    }

    public setVisibility(int visibility) {
        v.setVisibility(visibility);
        if(listener != null) {
            listener.onVisibilityChanged(visibility);
        }
    }

}

Then

 WatchedView v = new WatchedView(findViewById(R.id.myview));
 v.setListener(this);
vestlen
  • 1,103
  • 11
  • 17
Alexander Kulyakhtin
  • 47,782
  • 38
  • 107
  • 158
  • 1
    This is like just creating a helper class for the view. Overriding the view would be better than this in my opinion. But yes this would work, assuming that specific `setVisibility()` is being used every time – string.Empty Sep 25 '15 at 08:21
  • 3
    @Nicolas Tyler Submitter specifically said "without overriding the view" so your statement "Overriding the view would be better" is false. Besides, overriding the views will result in massive xml changes. In general your comment is devoid of meaning – Alexander Kulyakhtin Sep 25 '15 at 08:25
  • 1
    its not false, its just not what the OP asked for. The OP was looking for a listener for visibility changes. And preferably not having to override the view. – string.Empty Sep 25 '15 at 08:26
  • This solution seems to be better. It fits to all views. – no_cola Feb 19 '19 at 07:37
  • would prefer to use the first submission as this solution forces you each time you create a view you well need to subclass it. – Omar Beshary Oct 11 '21 at 22:12
  • 1
    This doesn't listen for visibility changes. It has no idea when visibility changes whenever something other than your decoration changes the visibility. – Trevor Feb 21 '23 at 22:31
2

Take a look at ViewTreeObserver.OnGlobalLayoutListener. As written in documentation, its callback method onGlobalLayout() is invoked when the global layout state or the visibility of views within the view tree changes. So you can try to use it to detect when view visibility changes.

greenfrvr
  • 643
  • 6
  • 19
0

We can match the visibility of TextView by a Flow/LiveData. Then listen to View visibility via the Flow/LiveData

class ViewModel : ViewModel() {

    private val _textNameVisibility = MutableStateFlow(View.VISIBLE)
    val textNameVisibility: LiveData<Int> = _textNameVisibility.asLiveData()


    fun setTextNameVisibility(visibility: Int) {
        _textNameVisibility.value = visibility
    }
}

On Activity/Fragment

viewModel.textNameVisibility.observe(this) {
    tvName.visibility = it
    Log.i("TAG", "View visibility change" + it)
}

Note that, we need to use setTextNameVisibility to change visibility of the View.
And StateFlow won't emit the same value (similar to distinctUntilChanged) so don't need care about previous visibility

Linh
  • 57,942
  • 23
  • 262
  • 279
-1
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import java.lang.ref.WeakReference


class MyImageView : ImageView {
    var mListener: WeakReference<VisibilityChangeListener>? = null

    interface VisibilityChangeListener {
        fun onVisibilityChanged(visibility: Int)
    }

    constructor(context: Context?) : super(context) {}
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}
    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
    }

    fun setVisibilityChangeListener(listener: VisibilityChangeListener) {
        mListener = WeakReference(listener)
    }

    override fun onVisibilityChanged(changedView: View, visibility: Int) {
        super.onVisibilityChanged(changedView, visibility)
        Toast.makeText(context,visibility.toString(), Toast.LENGTH_SHORT).show()

        if (mListener != null && changedView === this) {
            val listener = mListener!!.get()
            listener?.onVisibilityChanged(visibility)
        }
    }
}
habfarago
  • 1
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 07 '22 at 20:53