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!
Related
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
}
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.
I have already gone through several different available Questions related to this,
but not found very much useful.
Android TV. Adding custom views to VerticalGridFragment
How I can customize the View Inside the RowSupportFragment or Like is there any other way to achieve the same.
Are you using Presenter right? I think you need to use different types same way RecyclerView and Adapters has the itemViewType and transform your BaseCardView based on some rule, it could be the position or the type of it. I manage to do something like this for a row with the "View All" last card:
// This is just a simple FrameLayout with an ImageView or a TextView...
var firstItem: Any? = null
class PortraitTitleCardPresenter(context: Context) :
AbstractCardPresenter<BaseCardView>(context) {
override fun onCreateView(): BaseCardView {
val cardView = BaseCardView(context, null, R.style.BaseCard)
cardView.isFocusable = true
cardView.addView(LayoutInflater.from(context).inflate(R.layout.item_home, null))
return cardView
}
override fun onBindViewHolder(card: Any, cardView: BaseCardView) {
if(firstItem == null) {
firstItem = card
//Do something with firstItem...
}
if (card is ItemTitleType) {
cardView.image.visibility = View.VISIBLE
cardView.viewAll.visibility = View.GONE
Glide.with(context)
.load(card.imageUrl)
.centerCrop()
.apply(RequestOptions().placeholder(R.drawable.ic_wm_black)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
).into(cardView.image)
}
if (card is ViewAllType) {
cardView.image.visibility = View.GONE
cardView.viewAll.visibility = View.VISIBLE
}
}
}
I would like to do something like to control the "invisble state" of a View.
So I have a function that receive the View and there's a optional parameter invisibleType that can be View.INVISIBLE or View.GONE.
I would like to know how can I limit the options of this parameter for these two.
Like fun makeInvisible(view: View, invisibleType: View.INVISIBLE | View.GONE)
Can it be done with Kotlin?
Or my best option is create a custom enum or something like that to map the options to View.INVISBLE and View.GONE?
You can use enums or sealed classes to do it but you can use kotlin extension functions for more readability and understanding.
Make 3 extension function on View Object.
fun View.visible(): View {
this.visibility = View.VISIBLE
if (this is Group) {
this.requestLayout()
}
return this
}
fun View.inVisible(): View {
this.visibility = View.INVISIBLE
if (this is Group) {
this.requestLayout()
}
return this
}
fun View.gone(): View {
this.visibility = View.GONE
if (this is Group) {
this.requestLayout()
}
return this
}
then you can use like this
mView.layout_photoid_success.gone()
mView.layoutPhotoReview.visible()
Considering this:
MyView.setVisibility(View.VISIBLE)
can be simplified to this:
inline fun View.setVisible() = apply { visibility = View.VISIBLE }
MyView.setVisible()
Or this if you prefer:
inline infix fun View.vis(vis: Int) = apply { visibility = vis }
MyView vis View.VISIBLE
Is there anyway of accomplish the same by doing this:
MyView.VISIBLE
It seems a bit odd for a "getter" to modify state but you can use an extension property:
val View.VISIBLE: Unit
get() {
visibility = View.VISIBLE
}
And you could also make it return the new visibility value or return itself so that you can potentially chain calls.
val View.VISIBLE: Int
get() {
visibility = View.VISIBLE
return visibility
}
or
val View.VISIBLE: View
get() = apply { visibility = View.VISIBLE }
Yes, you can write an extension property property with a getter like this:
val View.visible: View
get() = apply { visibility = View.VISIBLE }
With the usage:
myView.visible
However, keep in mind that properties with side effects in getters are generally discouraged (see also: Functions vs Properties), and this behavior is rather confusing for a property.