How can I redraw an Anko frameLayout when my model changes? - android

I have an Anko component where I create my view with some code like this:
override fun createView(ui: AnkoContext<T>) = with(ui) {
frameLayout {
var imgView = imageView(R.drawable.ic_1).lparams {
horizontalMargin = ...
topMargin = ...
width = ...
height = ...
}
imgView.backgroundColor = gameModel.colour
}
}
The background of my imgView depends on a colour in my model.
Let's imagine I update my model elsewhere. How do I 'refresh' the Anko component UI to reflect the new gameModel.colour? I've never done any Android before but it seems one would usually use either invalidate() or requestLayout() but they don't seem to work.

Related

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

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
}

LazyList composable inside ViewPager2 with height fixation

I have a ViewPager2 inside a BottomSheetDialog in which I load a Fragment that contains a ComposeView. Inside this view I populate a LazyList with items as soon as they're loaded.
Now this works all fine, except that the ViewPager2 makes no height adaptions when it's inner contents change, so naturally I adapted the peekHeight at first and then added a GlobalLayoutListener to give the pager the height of the inner, currently displayed fragment view, like so:
val myPager = ...
myPager.registerOnPageChangeCallback(AdaptChildHeightOnPageChange(myPager))
...
internal class AdaptChildHeightOnPageChange(private val viewPager: ViewPager2) : ViewPager2.OnPageChangeCallback() {
private val otherViews = mutableSetOf<View>()
private fun getViewAtPosition(position: Int): View =
(viewPager.getChildAt(0) as RecyclerView).layoutManager?.findViewByPosition(position)
?: error("No layout manager set or no view found at position $position")
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val itemView = getViewAtPosition(position)
val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
itemView.updatePagerHeightForChild()
}
// remove the global layout listener from other views
otherViews.forEach { it.viewTreeObserver.removeOnGlobalLayoutListener(it.tag as ViewTreeObserver.OnGlobalLayoutListener) }
itemView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
itemView.tag = layoutListener
otherViews.add(itemView)
}
private fun View.updatePagerHeightForChild() {
post {
val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
measure(wMeasureSpec, hMeasureSpec)
if (viewPager.layoutParams.height != measuredHeight) {
viewPager.layoutParams = (viewPager.layoutParams as ViewGroup.LayoutParams)
.also { lp -> lp.height = measuredHeight }
}
}
}
}
(taken and adapted from https://stackoverflow.com/a/58632613/305532)
Now while this works fantastically with regular compose content, as soon as I switch my compose view to the LazyList implementation (or anything that uses Modifier.verticalScroll(...)), I receive the following exception:
Nesting scrollable in the same direction layouts like LazyColumn and \
Column(Modifier.verticalScroll()) is not allowed (Scroll.kt:370)
But I don't get this really, because I haven't nested any vertical-scolling compose elements that could trigger this exception. My only guess is that because of the height constraint I give to the ViewPager2 this internally triggers the enablement of vertical scrolling, making the inner LazyList unable to take over.
How can I solve this issue?
Ok, the crash seem to have stem from an issue with the GlobalLayoutListener. This constantly fired updates and kicked of relayouts, even though I tried to remove the listener explicitely before setting a new height to the surrounding pager.

How to create a View extension function in Kotlin

I'm trying to create an extension function in Kotlin. I did try several tutorials, but didn't quite understand, how to implement this one.
I'm trying to create a setWidth() function as such
//Find my_view in the fragment
val myView = v.findViewById<RelativeLayout>(R.id.my_view)
//Then use the extension function
myView.setNewWidth(500)
This is how I've defined my extension function
private fun View?.setNewWidth(i: Int) {
val layoutParams: ViewGroup.LayoutParams = View.layoutParams
layoutParams.width = i
View.layoutParams = layoutParams
}
I don't understand what I need to do here.
I want to call the extension function as myView.ExtensionFunction(), but I don't know how to do that. The tutorials, were un-informative.
I think the main problem here is how the extension function is defined, in particular, the lines that have View.layoutParams - this is calling a static property on View that doesn't exist. You need to use the one from the instance. If you'd write the extension function like so:
private fun View?.setNewWidth(i: Int) {
val layoutParams = this?.layoutParams
layoutParams?.width = i
this?.layoutParams = layoutParams
}
Then you can call the method like you want. Personally, I don't find this so readable and I'd remove the nullability here and write it as:
private fun View.setNewWidth(i: Int) {
val newLayoutParams = layoutParams
newLayoutParams?.width = i
layoutParams = newLayoutParams
}
The only difference is that now you need ?. to call the method if the view is nullable, which I personally find fine - myView?.setNewWidth(123). I assume most of the time you won't have a nullable view.
Ok, So my issue was that I didn't know how to get reference to the calling View. i.e., I didn't know how to call myView and set its property inside the extension function setNewWidth()
So, I tried using this? and it worked.
Then, I did a few changes to the extension function to work for myView which is a Relative Layout.
This is what I worked out:
private fun RelativeLayout?.setWidth(i: Int) {
val layoutParams: ViewGroup.LayoutParams? = this?.layoutParams
layoutParams?.width = i
this?.layoutParams = layoutParams
}

Is it possible to reuse a layout in Kotlin Anko

I read that the most benefit of using Anko is its reusability. But i could't find its exact example.
Currently in the new Android layout system, the boiler plate is like below:
DrawerLayout (with some setup)
CoordinatorLayout (with some setup)
AppBarLayout (with some setup)
ToolBar
<The Main Content>
NavigationView (with header inflated)
From the layout structure above, only <The Main Content> is varry. And
in many cases those ceremonial setup duplicated almost in every activity.
So here with Anko im thinking if there is a reusable solution about that issue. Im not expecting it will be reusable for general purpose layout, but et least i can minimize the ceremonial code in the project. Maybe i need something like:
class MainUI: AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View{
return with(ui) {
myCustomRootLayout {
//here is what <The Main Content> will be
}
}
}
}
From the code above im expecting myCustomRootLayout will do all the ceremonial setup for the root layout such as (DrawerLayout, CoordinatorLayout etc etc).
Is that possible?
EDIT
So i think my question is: How to make a custom component which can host other component
One way to reuse the code is to simply extract myCustomRootLayout into a extension method like so:
class MainUI: AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View {
return with(ui) {
myCustomRootLayout {
recyclerView()
}
}
}
}
fun <T> AnkoContext<T>.myCustomRootLayout(customize: AnkoContext<T>.() -> Unit = {}): View {
return relativeLayout {
button("Hello")
textView("myFriend")
customize()
}
}
However as stated in the documentation:
Although you can use the DSL directly (in onCreate() or everywhere
else), without creating any extra classes, it is often convenient to
have UI in the separate class. If you use the provided AnkoComponent
interface, you also you get a DSL layout preview feature for free.
It seems to be a good idea to extract the reusable piece into separate AnkoComponent:
class MainUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View {
return with(ui) {
MyCustomRootLayout<MainActivity>({
recyclerView()
}).createView(ui)
}
}
}
class MyCustomRootLayout<T : Context>(val customize: AnkoContext<T>.() -> Unit = {}) : AnkoComponent<T> {
override fun createView(ui: AnkoContext<T>) = with(ui) {
relativeLayout {
button("Hello")
textView("myFriend")
customize()
}
}
}
I actually found a way to do this, took me a while to figure it out.
I have a very basic test layout here, the content gets added to a
RelativeLayout.
The key here is to add your custom layout in a delegated AnkoContext that delegates to the immediate parent (the RelativeLayout in my case).
abstract class BaseAnkoComponent<T> : AnkoComponent<T> {
companion object {
val TOOLBAR_ID = View.generateViewId()
val COLLAPSING_ID = View.generateViewId()
val COORDINATOR_ID = View.generateViewId()
val APPBAR_ID = View.generateViewId()
val CONTENT_ID = View.generateViewId()
}
abstract fun <T> AnkoContext<T>.content(ui: AnkoContext<T>): View?
override fun createView(ui: AnkoContext<T>) = with(ui) {
coordinatorLayout {
id = COORDINATOR_ID
lparams(matchParent, matchParent)
appBarLayout(R.style.AppTheme_AppBarOverlay) {
id = APPBAR_ID
lparams(matchParent, wrapContent)
fitsSystemWindows = true
collapsingToolbarLayout {
id = COLLAPSING_ID
val collapsingToolbarLayoutParams = AppBarLayout.LayoutParams(matchParent, matchParent)
collapsingToolbarLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
layoutParams = collapsingToolbarLayoutParams
isTitleEnabled = false
toolbar {
id = TOOLBAR_ID
val toolbarLayoutParams = CollapsingToolbarLayout.LayoutParams(matchParent, dimenAttr(R.attr.actionBarSize))
toolbarLayoutParams.collapseMode = CollapsingToolbarLayout.LayoutParams.COLLAPSE_MODE_PIN
layoutParams = toolbarLayoutParams
minimumHeight = dimenAttr(R.attr.actionBarSize)
background = ColorDrawable(colorAttr(R.attr.colorPrimary))
popupTheme = R.style.AppTheme_PopupOverlay
}
}
}
with(AnkoContext.createDelegate(relativeLayout {
id = CONTENT_ID
val relativeLayoutParams = CoordinatorLayout.LayoutParams(matchParent, matchParent)
relativeLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
layoutParams = relativeLayoutParams
})) {
content(ui)
}
}
}
}
And then you can extend the BaseAnkoComponent and build your content in the same way with Anko DSL.
class FooActivityUi : BaseAnkoComponent<FooActivity>() {
override fun <T> AnkoContext<T>.content(): View? {
return verticalLayout {
lparams(width = matchParent, height = matchParent)
button("Hello")
textView("myFriend")
}
}
}
I am sure there is a better way to do this but I have not found it. Kinda new to Kotlin and Anko.

Horizontal LinearLayout in Anko

What is a good way to do a horizontalLayout in anko / kotlin ? verticalLayout works fine - could set orientation on it but it feels wrong. Not sure what I am missing there.
Just use a linearLayout() function instead.
linearLayout {
button("Some button")
button("Another button")
}
Yeah, LinearLayout is by default horizontal, but I tend to be extra specific and rather use a separate horizontalLayout function for that.
You can simply add the horizontalLayout function to your project:
val HORIZONTAL_LAYOUT_FACTORY = { ctx: Context ->
val view = _LinearLayout(ctx)
view.orientation = LinearLayout.HORIZONTAL
view
}
inline fun ViewManager.horizontalLayout(#StyleRes theme: Int = 0, init: _LinearLayout.() -> Unit): _LinearLayout {
return ankoView(HORIZONTAL_LAYOUT_FACTORY, theme, init)
}
I have opened a feature request at Anko: https://github.com/Kotlin/anko/issues/413

Categories

Resources