View container visibility based on the visibility of views contained in it - android

I have several views inside another view.
I need to show the container view if at least one view is visible. So, if none of the view's visibility is VISIBLE, then the container should itself hide.
It could be done by using constraintlayout group or any other ways in fragment.
But I am using Data Binding and I needed to handle it in ViewModel with LiveData. So I tried using MediatorLiveData. And it is not working as expected.
Here is how my code looks like:
class MyViewModel: ViewModel() {
val firstViewVisibility: LiveData<Int> = checkVisibility(firstView)
val secondViewVisibility: LiveData<Int> = checkVisibility(secondView)
val thirdViewVisibility: LiveData<Int> = checkVisibility(thirdView)
// and so on
val viewContainerVisibility = MediatorLiveData<Int>.apply {
fun update(visibility: Int) {
value = visibility
}
addSource(firstViewVisibility) {
update(it)
}
addSource(secondViewVisibility) {
update(it)
}
addSource(thirdViewVisibility) {
update(it)
}
// and so on
}
}
CheckVisibility function:
private fun checkVisibility(viewType: String) =
Transformations.map(myLiveData) { value ->
if(some logic involving value returns true) View.VISIBLE
else View.GONE
}
This is not working as the parent view's visibility depends upon the visibility added by last addSource in MediatorLiveData. So, if the last view's visibility is VISIBLE then the parent will be Visible and if it is GONE, the parent will be gone even though other view's visibility are VISIBLE.
Is MediatorLiveData not best fit here? OR I mis-utilized it?
What could be the best solution for my case?

Currently, when you update Visibility of the container, if the latest update of any view out of three is invisible, it set value as invisible even though previously any of three was visible. SO you need to update the Update() method. Something similar like this
val viewContainerVisibility = MediatorLiveData<Int>.apply {
fun update() {
if(firstViewVisibility.value == View.Visible || secondViewVisibility.value == View.Visible || thirdViewVisibility.value == View.Visible)
{View.Visible}
else{
View.GONE //or INVISIBLE as required}
}
addSource(firstViewVisibility) {
update()
}
addSource(secondViewVisibility) {
update()
}
addSource(thirdViewVisibility) {
update()
}
// and so on
}

Related

Android Espresso testing ProgressBar visibility GONE before assert

I'm trying to test a spinner that should display while loading the information from an API.
The problem is that I can't assert the initial state VISIBLE because it disappear too fast when the results are emitted back thus always having a failing test
Expected: (view has effective visibility <VISIBLE> and view.getGlobalVisibleRect() to return non-empty rectangle)
Got: view.getVisibility() was <GONE>
The first attempt using ui-automator
#Test
fun displayLoaderWhileFetchingPlaylistDetails() {
IdlingRegistry.getInstance().unregister(idlingResource)
uiObjectWithId(R.id.playlist_list).getChild(UiSelector().clickable(true).index(0)).click()
val spinner = uiObjectWithId(R.id.playlist_details_loader)
assertTrue(spinner.exists())
}
Another variant for the test without ui-automator
#Test
fun displayLoaderWhileFetchingPlaylistDetails2() {
IdlingRegistry.getInstance().unregister(idlingResource)
onView(
allOf(
withId(R.id.playlist_image),
isDescendantOfA(withPositionInParent(R.id.playlist_list, 0))
)
)
.perform(click())
assertDisplayed(R.id.playlist_details_loader)
}
ui-automator helper
fun uiObjectWithId(#IdRes id: Int): UiObject {
val resourceId = getTargetContext().resources.getResourceName(id);
val selector = UiSelector().resourceId(resourceId)
return UiDevice.getInstance(getInstrumentation()).findObject(selector)
}
Fragment
private fun observeLoaderState() {
viewModel.playlistLoader.observe(this as LifecycleOwner) { playlistSpinner ->
when (playlistSpinner) {
true -> playlist_details_loader.visibility = View.VISIBLE
else -> playlist_details_loader.visibility = View.GONE
}
}
}
ViewModel
class PlaylistDetailViewModel(
private val repository: PlaylistRepository
) : ViewModel() {
val playlistLoader = MutableLiveData<Boolean>()
fun getPlaylistDetails(playlistId: String) = liveData {
playlistLoader.postValue(true)
emitSource(
repository.getPlaylistDetailsById(playlistId)
.onEach { playlistLoader.postValue(false) }
.asLiveData()
)
}
}
Thanks!
In Android when you set a View's visibility to GONE the renderer does not draw the View object, so the View practically has no dimensions. The same applies if you call any function that searchs in the UI tree for the View that has visibility set to GONE, and will return no match. If your only goal is to pass the test, my suggestions would be to set the View to INVISIBLE instead of GONE or to change the way you test for that specific layout.
From Android documentation:
View.GONE This view is invisible, and it doesn't take any space for
layout purposes.
View.INVISIBLE This view is invisible, but it still takes up space
for layout purposes.

RecyclerView sets wrong MotionLayout state for its items

First: I created a sample project showing this problem. By now I begin to think that this is a bug in either RecyclerView or MotionLayout.
https://github.com/muetzenflo/SampleRecyclerView
This project is set up a little bit different than what is described below: It uses data binding to toggle between the MotionLayout states. But the outcome is the same. Just play around with toggling the state and swiping between the items. Sooner than later you'll come upon a ViewHolder with the wrong MotionLayout state.
So the main problem is:
ViewHolders outside of the screen are not updated correctly when transition from one MotionLayout state to another.
So here is the problem / What I've found so far:
I am using a RecyclerView.
It has only 1 item type which is a MotionLayout (so every item of the RV is a MotionLayout).
This MotionLayout has 2 states, let's call them State big and State small
All items should always have the same State. So whenever the state is switched for example from big => small then ALL items should be in small from then on.
But what happens is that the state changes to small and most(!) of the items are also updated correctly. But one or two items are always left with the old State. I am pretty sure it has to do with recycled ViewHolders. These steps produce the issue reliably when using the adapter code below (not in the sample project):
swipe from item 1 to the right to item 2
change from big to small
change back from small to big
swipe from item 2 to the left to item 1
=> item 1 is now in the small state, but should be in the big state
Additional findings:
After step 4 if I continue swiping to the left, there comes 1 more item in the small state (probably the recycled ViewHolder from step 4). After that no other item is wrong.
Starting from step 4, I continue swiping for a few items (let's say 10) and then swipe all the way back, no item is in the wrong small state anymore. The faulty recycled ViewHolder seems to be corrected then.
What did I try?
I tried to call notifyDataSetChanged() whenever the transition has completed
I tried keeping a local Set of created ViewHolders to call the transition on them directly
I tried to use data-binding to set the motionProgress to the MotionLayout
I tried to set viewHolder.isRecycable(true|false) to block recycling during the transition
I searched this great in-depth article about RVs for hint what to try next
Anyone had this problem and found a good solution?
Just to avoid confusion: big and small does not indicate that I want to collapse or expand each item! It is just a name for different arrangement of the motionlayouts' children.
class MatchCardAdapter() : DataBindingAdapter<Match>(DiffCallback, clickListener) {
private val viewHolders = ArrayList<RecyclerView.ViewHolder>()
private var direction = Direction.UNDEFINED
fun setMotionProgress(direction: MatchCardViewModel.Direction) {
if (this.direction == direction) return
this.direction = direction
viewHolders.forEach {
updateItemView(it)
}
}
private fun updateItemView(viewHolder: RecyclerView.ViewHolder) {
if (viewHolder.adapterPosition >= 0) {
val motionLayout = viewHolder.itemView as MotionLayout
when (direction) {
Direction.TO_END -> motionLayout.transitionToEnd()
Direction.TO_START -> motionLayout.transitionToStart()
Direction.UNDEFINED -> motionLayout.transitionToStart()
}
}
}
override fun onBindViewHolder(holder: DataBindingViewHolder<Match>, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
val itemView = holder.itemView
if (itemView is MotionLayout) {
if (!viewHolders.contains(holder)) {
viewHolders.add(holder)
}
updateItemView(holder)
}
}
override fun onViewRecycled(holder: DataBindingViewHolder<Match>) {
if (holder.adapterPosition >= 0 && viewHolders.contains(holder)) {
viewHolders.remove(holder)
}
super.onViewRecycled(holder)
}
}
I made some progress but this is not a final solution, it has a few quirks to polish. Like the animation from end to start doesn't work properly, it just jumps to the final position.
https://github.com/fmatosqg/SampleRecyclerView/commit/907ec696a96bb4a817df20c78ebd5cb2156c8424
Some things that I changed but are not relevant to the solution, but help with finding the problem:
made duration 1sec
more items in recycler view
recyclerView.setItemViewCacheSize(0) to try to keep as few unseen items as possible, although if you track it closely you know they tend to stick around
eliminated data binding for handling transitions. Because I don't trust it in view holders in general, I could never make them work without a bad side-effect
upgraded constraint library with implementation "androidx.constraintlayout:constraintlayout:2.0.0-rc1"
Going into details about what made it work better:
all calls to motion layout are done in a post manner
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
Compared actual vs desired properties
val goalProgress =
if (currentState) 1f
else 0f
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
if (motionLayout.currentState != desiredState) {
safeRunBlock {
startTransition(currentState)
}
}
}
This would be the full class of the partial solution
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
val motionLayout: MotionLayout =
binding.root.findViewById<MotionLayout>(R.id.root_item_recycler_view)
.also {
it.setTransitionDuration(1_000)
it.setDebugMode(DEBUG_SHOW_PROGRESS or DEBUG_SHOW_PATH)
}
var lastPosition: Int = -1
fun bind(item: T, position: Int, layoutState: Boolean) {
if (position != lastPosition)
Log.i(
"OnBind",
"Position=$position lastPosition=$lastPosition - $layoutState "
)
lastPosition = position
setMotionLayoutState(layoutState)
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
// https://stackoverflow.com/questions/51929153/when-manually-set-progress-to-motionlayout-it-clear-all-constraints
fun safeRunBlock(block: () -> Unit) {
if (ViewCompat.isLaidOut(motionLayout)) {
block()
} else {
motionLayout.post(block)
}
}
fun setMotionLayoutState(currentState: Boolean) {
val goalProgress =
if (currentState) 1f
else 0f
safeRunBlock {
startTransition(currentState)
}
if (motionLayout.progress != goalProgress) {
val desiredState =
if (currentState) motionLayout.startState
else motionLayout.endState
if (motionLayout.currentState != desiredState) {
Log.i("Pprogress", "Desired doesn't match at position $lastPosition")
safeRunBlock {
startTransition(currentState)
}
}
}
}
fun startTransition(currentState: Boolean) {
if (currentState) {
motionLayout.transitionToStart()
} else {
motionLayout.transitionToEnd()
}
}
}
Edit: added constraint layout version

ViewSwitcher not switching view with .showNext() at initialization, after Livedata change is observed

I have a ViewSwitcher inside a DialogFragment. The switcher holds 2 views, a "1. selection view" and a "2. selected view".
The initial view is the defaulted to "1. selection view". When fromType is non-zero from the observing event database, I want the Livedata observer to call dialogViewSwitcher.showNext(), but the showNext() function is not called on dialog's initialization.
DialogFragment, inside onCreateDialog:
viewModel.event.observe(this, Observer {
it?.let {
viewModel.setCurrentFromType(it.fromPlaceType)
}
})
viewModel.currentFromType.observe(this, Observer {
if (it == 0) {
viewModel.setCurrentFromStatus(false)
} else {
viewModel.setCurrentFromStatus(true)
}
})
viewModel.currentFromStatus.observe(this, Observer {
binding.dialogViewSwitcher.showNext()
})
ViewModel:
private val _currentFromType = MutableLiveData(0)
val currentFromType: LiveData<Int>
get() = _currentFromType
fun setCurrentFromType(fromType: Int) {
_currentFromType.value = fromType
}
private val _currentFromStatus = MutableLiveData(false)
val currentFromStatus: LiveData<Boolean>
get() = _currentFromStatus
fun setCurrentFromStatus(status: Boolean) {
_currentFromStatus.value = status
}
Log shows the currentFromStatus observed the change (from false to true) when the dialog opens, but I'm thinking that because the dialogViewSwitcher hasn't initialized yet inside onCreatDialog when the change to currentFromStatus was observed, so showNext() did nothing. I also looked into data binding and didn't find a ViewSwitcher property in xml that shows the second view. What should I do to fix this behaviour?
Didn't find a good way to programmatically showNext() at onCreate. Instead I deleted the viewswitcher and use databinding on the visibility of each of the 2 child views, and this way works well.

How to change view of expandable list view children when parent is collapsed in Android?

I am implementing a selection mode in ExpandableListView. The selection toggles when I click a child. I have a CheckBox in each parent, from which I want to control the selection of all the children at once.
My problem is that when the parent is collapsed and I click its CheckBox, the app crashes due to null pointer exception because when I try to change the selection of the children, I can't find the children and get null. But everything works fine when the parent is expanded.
So, what is a good approach to tackle such kind of problem?
I solved by adding some lines of code without changing the previous code, so this answer may be helpful for someone who doesn't want to rewrite the code with a different approach.
In the calling Fragment or Activity, where the Adapter is being set, add:
private val isMyGroupExpanded = SparseBooleanArray()
val MyAdapterEV = AdapterEV(/* params */) { isChecked, groupPosition ->
changeSelection(isChecked, groupPosition)
}
// record which groups are expanded and which are not
MyAdapterEV.setOnGroupExpandListener { i -> isMyGroupExpanded.put(i, true) }
MyAdapterEV.setOnGroupCollapseListener { i -> isMyGroupExpanded.put(i, false) }
// and where changing the selection of child
private fun changeSelection(isChecked: Boolean, groupPosition: Int) {
if (isMyGroupExpanded.get(groupPosition, false)) {
/* change only if the group is expanded */
}
}
So, the children of the collapsed group are not affected, but they are needed to be changed when the group expands, for that, add some lines of code in the Adapter:
private val isGroupChecked = SparseBooleanArray()
// inside override fun getGroupView(...
MyCheckBoxView.setOnCheckedChangeListener { _, isChecked ->
onCheckedChangeListener(isChecked, groupPosition)
isGroupChecked.put(groupPosition, isChecked)
}
// inside override fun getChildView(...
if (isGroupChecked.contains(groupPosition)) {
myView.visibility = if (isGroupChecked.get(groupPosition)) View.VISIBLE else View.INVISIBLE
}

How to group a bunch of views and change their visibility together

I have an activity which contains 2 group of views, which CAN'T be located into same LAYOUT group but belong to same LOGIC group, meaning that they should be shown or hidden and bind click event at same time. The thing is that I feel really awful to write something like this:
fun hide() {
view1.visibility = View.GONE
view2.visibility = View.GONE
view3.visibility = View.GONE
// ...
view9.visibility = View.GONE
}
fun show() {
view1.visibility = View.VISIBLE
view2.visibility = View.VISIBLE
view3.visibility = View.VISIBLE
// ...
view9.visibility = View.VISIBLE
view1.setOnClickListener{ run() }
view2.setOnClickListener{ run() }
view3.setOnClickListener{ run() }
// ...
view9.setOnClickListener{ run() }
}
I did read a post which describes a kotlin skill to simplify this mess by somehow grouping those views then just handle the groups, but unfortunately I can no longer find that post..
Help would be appreciated!
========= Update 2019-07-31 =========
I found the solution but forgot to update this question, the 'grouping' I was looking for, is in fact not a Kotlin specific feature but simply using vararg, and we can use Kotlin extension (which is AWESOME) to simplify a bit more:
// assume we have a base activity or fragment, then put below functions in there
fun View.show() {
visibility = View.VISIBLE
}
fun show(vararg views: View) {
views.forEach { it.show() }
}
fun View.hide() {
visibility = View.GONE
}
fun hide(vararg views: View) {
views.forEach { it.hide() }
}
// then in any activity or fragment
show(v1, v2, v3, v4)
v9.hide()
============= updated 2020-03-07 ================
This is exactly androidx.constraintlayout.widget.Group designed to do, which can logically group a bunch of views from anywhere and control their visibility by only changing group's visibility.
Since ConstraintLayout 1.1 you can use Group instead of LayoutGroup.
You can simply add this code to you XML layout
<android.support.constraint.Group
android:id="#+id/profile"
app:constraint_referenced_ids="profile_name,profile_image" />
And then you can call it from code to achieve behavior, that you need
profile.visibility = GONE
profile.visibility = VISIBLE
For more details read this article https://medium.com/androiddevelopers/introducing-constraint-layout-1-1-d07fc02406bc
You need to create extension functions.
For example:
fun View.showGroupViews(vararg view: View) {
view.forEach {
it.show()
}
}
fun View.hideGroupViews(vararg view: View) {
view.forEach {
it.hide()
}
}
Create a list of views and loop on it
val views = listOf<View>(view1, view2, ...)
views.forEach {
it.visibility = View.GONE
}
You can also create extension function of Iterable<View> to simplify any kind of action on listed views
fun Iterable<View>.visibility(visibility: Int) = this.forEach {
it.visibility = visibility
}
//usage
views.visibility(View.GONE)
Maybe you want to locate all views from tags in XML. Take a look at this answer
Depending on how is your layout structured you might want to group those views in a ViewGroup like LinearLayout, RelativeLayout, FrameLayout or ConstraintLayout.
Then you can change visibility just on the ViewGroup and all of its children will change it too.
Edit:
Without ViewGroup the only solution to eliminating this boilerplate is to enable databinding in your project and set it like this:
In your Activity/Fragment:
val groupVisible = ObservableBoolean()
fun changeVisibility(show: Boolean) {
groupVisible.set(show)
}
In your xml:
<layout>
<data>
<variable name="groupVisible" type="Boolean"/>
</data>
<View
android:visibility="#{groupVisible ? View.VISIBLE : View.GONE}"/>
</layout>
Why don't you create an array:
val views = arrayOf(view1, view2, view3, view4, view5, view6, view7, view8, view9)
then:
fun show() {
views.forEach {
it.visibility = View.VISIBLE
it.setOnClickListener{ }
}
}
fun hide() {
views.forEach { it.visibility = View.INVISIBLE }
}
Or without an array if the names of the views are surely like view1, view2, ...
for (i in 1..9) {
val id = resources.getIdentifier("view$i", "id", context.getPackageName())
val view = findViewById<View>(id)
view.visibility = View.VISIBLE
view.setOnClickListener{ }
}
You can define a function with three parameters and use vararg like following code:
fun changeVisiblityAndAddClickListeners(visibility: Int,
listener: View.OnClickListener,
vararg views: View) {
for (view: View in views) {
view.visibility = visibility
if (visibility == View.VISIBLE) {
view.setOnClickListener(listener)
}
}
}
Of course if you have too many views, this is not a effective solution. I just added this code snippet for an alternative way especially for problems with dynamic view set.
If your views are not inside a view group you can use an extension function. You could create one to toggle the visibility of the views:
fun View.toggleVisibility() {
if (visibility == View.VISIBLE) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
}
}
And you can use it like this:
view.toggleVisibility()
First in your xml layout, group your views by android:tag="group_1" attribute.
Then inside your activity use a for loop to implement whatever logic you need:
val root: ViewGroup = TODO("find your root layout")
for (i in 0 until root.childCount) {
val v = root.getChildAt(i)
when (v.tag) {
"group_1" -> {
TODO()
}
"group_2" -> {
TODO()
}
else -> {
TODO()
}
}
}
You can create a LinearLayout or any other ViewGroup containing your child Views and give it an ID in the XML file, then in your Activity or Fragment class define these functions:
fun disableViewGroup(viewGroup: ViewGroup) {
viewGroup.children.iterator().forEach {
it.isEnabled = false
}
}
fun enableViewGroup(viewGroup: ViewGroup) {
viewGroup.children.iterator().forEach {
it.isEnabled = true
}
}
And then in onCreate() or onStart() call it as following:
disableViewGroup(idOfViewGroup)
The children method returns a Sequence of children Views in the ViewGroup which you can iterate by forEach and apply whatever operation applicable to Views.
Hope it helps!

Categories

Resources