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 :)
Related
I've been trying to learn MVVM and I have a question regarding the kind of logic that is acceptable for a fragment to contain in order to follow the pattern.
For example when showing a loader, should it be separated into two different states, or is it acceptable to have one state with a boolean argument and for the fragment to handle that logic?
So in the view model should the code be like this
fun makeCall() {
_state.value = Loading(true)
// make the call
_state.value = Loading(false)
}
and in the fragment have the check
private fun handleLoadingState(loading: Boolean) {
if (loading) {
binding.loader.visibility = View.VISIBLE
} else {
binding.loader.visibility = View.GONE
}
}
or like this?
fun makeCall() {
_state.value = ShowLoader
// make the call
_state.value = HideLoader
}
and this
private fun handleShowLoaderState() {
binding.loader.visibility = View.VISIBLE
}
private fun handleHideLoaderState() {
binding.loader.visibility = View.GONE
}
If both ways are correct is maybe one of them considered better? Or it doesn't really matter which one I use?
I have a simple fade out animation function for my android app, it works, but the issue I am having is that, after the fade out animation has run, the view (TextView in this case) does not stay faded out, the alpha value becomes 1 again.
Here is the fadeOut function code:
fun fadeOut(duration: Long = 100) : AlphaAnimation{
val fadeOut = AlphaAnimation(1f, 0f)
fadeOut.interpolator = DecelerateInterpolator()
fadeOut.duration = duration
return fadeOut
}
And I use it like this:
myTextView.startAnimation(fadeOut(500))
Any help or advice will be highly appreciated.
I think Animation::setFillAfter function would do the trick for you, the code would be like this:
val animation = fadeOut(500)
animation.fillAfter = true
myTextView.startAnimation(animation)
Although, this solution only preserves the alpha value of the view after the animation ends, if you want to change the visibility of the view, you need to change it when the animation ends using Animation.AnimationListener interface, the code would be like this:
myTextView.startAnimation(fadeOut(500).apply {
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
myTextView.visibility = View.GONE
}
override fun onAnimationRepeat(animation: Animation?) {
}
})
})
You can set visibility in handler after running animation
myTextView.startAnimation(fadeOut(500))
Handler().postDelayed({
myTextView.visibility = View.GONE
},500)
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
}
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()
}
}
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.