How to use Kotlin Android Extensions with ViewStub properly? - android

Does the extensions have a magic to call inflated view? As far as I see, I should break the harmony of code and call findViewById.
The intent was to inflate layout_ongoingView layout at sometime, and make hidden, and visible again based on scenario.
<ViewStub
android:id="#+id/viewStubOngoing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="#+id/ongoingView"
android:layout="#layout/layout_ongoingView" />
And codes
override fun hideOngoingPanel() {
if (viewStubOngoing !is ViewStub) {
findViewById(R.id.ongoingView).visibility = View.GONE
}
}
override fun showOngoingPanel() {
if (viewStubOngoing !is ViewStub) {
findViewById(R.id.ongoingView).visibility = View.VISIBLE
} else {
viewStubOngoing.inflate()
}
}

Related

What is an elegant way of toggling the visibility of UI sections?

I build Android apps using the MVP pattern and I'm often breaking up my UI into various sections like this:
<ConstraintLayout
android:id="#+id/recyclerSection"
<ConstraintLayout
android:id="#+id/errorSection"
<ConstraintLayout
android:id="#+id/emptySection"
Then in my presenter I'll call
view.showError()
and my view ends up with functions that look like this:
override fun showError(){
recyclerSection.visibility = View.GONE
errorSection.visibility = View.VISIBLE
emptySection.visibility = View.GONE
}
override fun showList(){
recyclerSection.visibility = View.VISIBLE
errorSection.visibility = View.GONE
emptySection.visibility = View.GONE
}
Is there a more elegant way to code this to achieve this toggling of view sections?
What do you mean with elegant? Here's an fade extension which is pretty nice
fun View.fadeToVisible(time: Long) {
alpha = 0f
animate()
.alpha(1f)
.setDuration(time)
.withStartAction {
visibility = View.VISIBLE
}
.start()
}
fun View.fadeToGone(time: Long) {
alpha = 1f
animate()
.alpha(0f)
.setDuration(time)
.withEndAction {
visibility = View.GONE
}
.start()
}
And then you use it like this (with 200 ms time):
override fun showError(){
recyclerSection.fadeToGone(200)
errorSection.fadeToVisible(200)
emptySection.fadeToGone(200)
}
override fun showList(){
recyclerSection.fadeToVisible(200)
errorSection.fadeToGone(200)
emptySection.fadeToGone(200)
}
However it's a matter of taste :)

Force parameter to be specific types that can be passed into a method

Android 3.5
Kotlin 1.3
I have the following method that passes in a parameter that could be VISIBLE, INVISIBLE, or GONE
fun setPromotionVisibility(Int: toVisiblity) {
tvPromoation.visibility = toVisibility
}
However, when I call this method I could pass in any Int that might not be a visibility i.e.
setPromotionVisibility(234)
instead of doing this:
setPromotionVisibility(View.VISIBLE)
Just wondering if there anything I could do to force the user of the method to only enter VISIBLE, INVISIBLE, or GONE
Many thanks in advance
You can create a type-safe approach with an enum:
enum class Visibility(
val asInt: Int
) {
VISIBLE(View.VISIBLE),
INVISIBLE(View.INVISIBLE),
GONE(View.GONE),
}
which you then use as a parameter type:
fun setPromotionVisibility(toVisiblity: Visibility) {
tvPromoation.visibility = toVisibility.asInt
}
Use Annotation for this
#IntDef({View.VISIBLE, View.INVISIBLE, View.GONE})
#Retention(RetentionPolicy.SOURCE)
public #interface Visibility {}
fun setPromotionVisibility(#Visibility toVisiblity: Int) {
tvPromoation.visibility = toVisibility
}
I don't know if it is useful for your case, but in my projects, I almost never use INVISIBLE.
So, I made an extension function
fun View.visible(value: Boolean) {
visibility = if (value) View.VISIBLE else View.GONE
}
It also can be better:
fun View.visible(value: Boolean, animated: Boolean = false) {
if (animated) {
if (value) animate().alpha(1F).withStartAction { visibility = View.VISIBILE }
else animate().alpha(0F).withEndAction { visibility = View.GONE }
} else visibility = if (value) View.VISIBLE else View.GONE
}

Android Espresso testing SwipeRefreshLayout OnRefresh not been triggered on swipeDown

I'm trying to write simple test for pull to refresh as a part of integration testing. I'm using the newest androidX testing components and Robolectric. I'm testing isolated fragment in which one I'm injecting mocked presenter.
XML layout part
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerTasks"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Fragment part
binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
presenter.onRefresh();
}
});
Test:
onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();
but test doesn't pass, message:
Wanted but not invoked: presenter.onRefresh();
The app works perfectly fine and pull to refresh calls presenter.onRefresh(). I did also debugging of the test and setOnRefreshListener been called and it's not a null. If I do testing with custom matcher to check the status of SwipeRefreshLayout test passes.
onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));
I did some minor investigation over last weekend since I was facing the same issue and it was bothering me. I also did some comparing with what happens on a device to spot the differences.
Internally androidx.swiperefreshlayout.widget.SwipeRefreshLayout has an mRefreshListener that will run when onAnimationEnd is called. The AnimationEnd will trigger then OnRefreshListener.onRefresh method.
That animation listener (mRefreshListener) is passed to the mCircleView (CircleImageView) and the circle animation start is called.
On a device when the view draw method is called it will call the applyLegacyAnimation method that will, in turn, call the AnimationStart method. At the AnimationEnd, the onRefresh method will be called.
On Robolectric the draw method of the View is never called since the items are not actually drawn. This means that the animation will never run and thus neither will the onRefresh method.
My conclusion is that with the current version of Robolectric is not possible to verify that the onRefresh called due to implementation limitations. It seems though that it is planned to have a realistic rendering in the future.
I'm finally able to solve this using a hacky way :
fun swipeToRefresh(): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return object : BaseMatcher<View>() {
override fun matches(item: Any): Boolean {
return isA(SwipeRefreshLayout::class.java).matches(item)
}
override fun describeMismatch(item: Any, mismatchDescription: Description) {
mismatchDescription.appendText(
"Expected SwipeRefreshLayout or its Descendant, but got other View"
)
}
override fun describeTo(description: Description) {
description.appendText(
"Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
)
}
}
}
override fun getDescription(): String {
return "Perform swipeToRefresh on the SwipeRefreshLayout"
}
override fun perform(uiController: UiController, view: View) {
val swipeRefreshLayout = view as SwipeRefreshLayout
swipeRefreshLayout.run {
isRefreshing = true
// set mNotify to true
val notify = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mNotify"
}
notify?.isAccessible = true
if (notify is KMutableProperty<*>) {
notify.setter.call(this, true)
}
// mockk mRefreshListener onAnimationEnd
val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
it.name == "mRefreshListener"
}
refreshListener?.isAccessible = true
val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
animatorListener.onAnimationEnd(mockk())
}
}
}
}

Can't set visibility on constraint group

When i try to set the visibility of the group on button click,it doesn't affect the view's visibility.Using com.android.support.constraint:constraint-layout:1.1.0-beta4. I've tried setting it element-wise without problems,but no success with the group.
My MainActivity.kt
private fun toggleLoginUI(show: Boolean) {
if (show) {
group.visibility = VISIBLE
} else {
group.visibility = INVISIBLE
}
}
fun onClick(view: View) {
when (view.id) {
R.id.button -> toggleLoginUI(true)
R.id.button4 -> toggleLoginUI(false)
}
}
My activity_main.xml
<android.support.constraint.ConstraintLayout..
<TextView
android:id="#+id/textView"
... />
<TextView
android:id="#+id/textView2"
... />
<Button
android:id="#+id/button"
.../>
<Button
android:id="#+id/button4"
... />
<android.support.constraint.Group
android:id="#+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="textView,textView2" />
</android.support.constraint.ConstraintLayout>
Update: This was reported as fixed in ConstraintLayout version 2.0.0 beta 6. See bug fixes for ConstraintLayout 2.0.0 beta 6 .
This looks like a bug to me. GONE works but INVISIBLE doesn't and I think it should. It may be worth a bug report unless someone can post where my thinking is wrong. (I am using constraint-layout:1.1.0-beta4.)
In the meantime, here is a work-around that explicitly retrieves the ids within the group and sets the visibility of each retrieved view.
Within MainActivity.kt
private fun toggleLoginUI(show: Boolean) {
if (show) {
setGroupVisibility(mLayout, group, Group.VISIBLE)
} else {
setGroupVisibility(mLayout, group, Group.INVISIBLE)
}
}
private fun setGroupVisibility(layout: ConstraintLayout, group: Group, visibility: Int) {
val refIds = group.referencedIds
for (id in refIds) {
layout.findViewById<View>(id).visibility = visibility
}
}
mLayout is the ConstraintLayout.
Update: Here is another work-around that takes advantage of the fact that changing to/from GONE works as expected:
private fun toggleLoginUI(show: Boolean) {
if (show) {
group.visibility = GONE
group.visibility = VISIBLE
} else {
group.visibility = GONE
group.visibility = INVISIBLE
}
}
You can also simply call requestLayout method after changing Group visibility to View.INVISIBLE.
fun makeGroupInvisible(group: Group) {
group.visibility = View.INVISIBLE
group.requestLayout()
}
Problem is that android.support.constraint.Group updates visibility of its members in updatePreLayout method which is called from onMeasure in ConstraintLayout.
android.support.constraint.Group has a public method
public void updatePreLayout(ConstraintLayout container) {
...
}
that updates children visibilities, so calling
dataGroup.visibility = if (visible) View.VISIBLE else View.INVISIBLE
dataGroup.updatePreLayout(root)
worked for me
Had the same problem and nothing from above helps. My solution was to setVisibility(viewId, ConstraintSet.VISIBLE) inside constraint set and apply that to ConstraintLayout view.
For example:
myContstraintSet.apply {
setVisibility(firstGroup.id, ConstraintSet.VISIBLE)
setVisibility(secondGroup.id, ConstraintSet.GONE)
connect(oneView.id, ConstraintSet.BOTTOM, R.id.secondView, ConstraintSet.TOP)
clear(anotherView.id, ConstraintSet.TOP)
}
myContstraintSet.applyTo(myConstraintLayout)
just add follow line you can change it .
so it visible .
group.visibility=ConstraintLayout.GONE
Just clean your project or Rebuild your project

BottomSheetBehaviour setstate without animation

I have tried the new BottomSheetBehaviour with design library 23.0.2 but i think it too limited. When I change state with setState() method, the bottomsheet use ad animation to move to the new state.
How can I change state immediately, without animation? I don't see a public method to do that.
Unfortunately it looks like you can't. Invocation of BottomSheetBehavior's setState ends with synchronous or asynchronous call of startSettlingAnimation(child, state). And there is no way to override these methods behavior cause setState is final and startSettlingAnimation has package visible modifier. Check the sources for more information.
I have problems with the same, but in a bit different way - my UI state changes setHideable to false before that settling animation invokes, so I'm getting IllegalStateException there. I will consider usage of BottomSheetCallback to manage this properly.
If you want to remove the show/close animation you can use dialog.window?.setWindowAnimations(-1). For instance:
class MyDialog(): BottomSheetDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setDimAmount(0f) // for removing the dimm
dialog.window?.setWindowAnimations(-1) // for removing the animation
return dialog
}
}
If you really need it, then you can resort to reflection:
fun BottomSheetBehavior.getViewDragHelper(): ViewDragHelper? = BottomSheetBehavior::class.java
.getDeclaredField("viewDragHelper")
.apply { isAccessible = true }
.let { field -> field.get(this) as? ViewDragHelper? }
fun ViewDragHelper.getScroller(): OverScroller? = ViewDragHelper::class.java
.getDeclaredField("mScroller")
.apply { isAccessible = true }
.let { field -> field.get(this) as? OverScroller? }
Then you can use these extension methods when the state changes:
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onSlide(view: View, offset: Float) {}
override fun onStateChanged(view: View, state: Int) {
if (state == STATE_SETTLING) {
try {
bottomSheetBehavior.getViewDragHelper()?.getScroller()?.abortAnimation()
} catch(e: Throwable) {}
}
}
})
I will add that the code is not perfect, getting fields every time the state changes is not efficient, and this is done for the sake of simplicity.

Categories

Resources