I want to animate TabLayout dot indicator to stretch it on selected state. I used default animation to do that but its bugged and it cant take default shape by animating width only. After animation its stretched to rectangle and I don't understand why.
Code:
val vg = tabLayout.getChildAt(0) as ViewGroup
tabLayout.addOnTabSelectedListener(object: TabLayout.ViewPagerOnTabSelectedListener(viewPager){
override fun onTabReselected(tab: TabLayout.Tab) {}
override fun onTabUnselected(tab: TabLayout.Tab) {
val tabDot = vg.getChildAt(tab.position)
tabDot?.let { v->
animateDotWidthDefault(v)
animateDotColor(v, R.color.selected_blue, R.color.default_grey)
}
}
override fun onTabSelected(tab: TabLayout.Tab) {
val tabDot = vg.getChildAt(tab.position)
tabDot?.let { v->
animateDotWidthStretch(v)
animateDotColor(v, R.color.default_grey, R.color.selected_blue)
}
}
})
private fun animateDotWidthDefault(tabDot: View){
val widthAnimator = ValueAnimator.ofInt(stretchedWidth, defaultWidth)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
tabDot.layoutParams.width = animation.animatedValue as Int
tabDot.requestLayout()
}
widthAnimator.start()
}
private fun animateDotWidthStretch(tabDot: View){
val widthAnimator = ValueAnimator.ofInt(defaultWidth, stretchedWidth)
widthAnimator.duration = 500
widthAnimator.interpolator = AccelerateInterpolator()
widthAnimator.apply {
addUpdateListener { animation ->
tabDot.layoutParams.width = animation.animatedValue as Int
tabDot.requestLayout()
}
}
widthAnimator.start()
}
TabIndicator:
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<item android:gravity="center">
<shape
android:shape="oval"
android:useLevel="false">
<solid android:color="#color/selected_blue" />
<size
android:width="8dp"
android:height="8dp"/>
</shape>
</item>
</layer-list>
Goal:
Reality:
The bug could definitely be related to the
android:fillAfter="true"
android:fillEnabled="true"
configurations on the XML. Could you paste the original XML and the container so we can edit our answer and give a more accurate solution to your issue.
Great component by the way, I tested it on my end and it works perfect with this XML config:
<!-- For demonstration / testing purposes -->
<com.google.android.material.tabs.TabLayout
android:id="#+id/indicator3"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="14dp"
android:layout_marginBottom="64dp"
android:clipChildren="false"
android:clipToPadding="false"
app:tabBackground="#drawable/tablayout_unselected"
app:tabGravity="center"
app:tabIndicator="#drawable/tablayout_selected2"
app:tabIndicatorAnimationDuration="250"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="#color/xxxRed"
app:tabIndicatorFullWidth="false"
app:tabIndicatorGravity="bottom"
app:tabIndicatorHeight="6dp"
app:tabMode="scrollable"
android:fillAfter="true"
android:fillEnabled="true"
app:tabRippleColor="#color/carbon_red_a700"
app:tabSelectedTextColor="#color/black"
app:tabTextColor="#android:color/black"
app:tabUnboundedRipple="true">
<com.google.android.material.tabs.TabItem
android:id="#+id/firstTab"
android:layout_width="12dp"
android:layout_height="wrap_content" />
<com.google.android.material.tabs.TabItem
android:id="#+id/secondTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.tabs.TabItem
android:id="#+id/thirdTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.tabs.TabItem
android:id="#+id/fourthTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.tabs.TabItem
android:id="#+id/fifthTab"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</com.google.android.material.tabs.TabLayout>
edit1:
I just figured out that even though you didn't provide the animateDotColor() function, ChatGPT took a guess and provided this missing function.
It turns out it is exactly this function that's causing the trouble for you, removing the rounded corners.
private fun animateDotColor(tabDot: View, fromColor: Int, toColor: Int) {
val colorAnimator = ValueAnimator.ofObject(
ArgbEvaluator(),
ContextCompat.getColor(tabDot.context, fromColor),
ContextCompat.getColor(tabDot.context, toColor)
)
colorAnimator.duration = 500
colorAnimator.interpolator = DecelerateInterpolator()
colorAnimator.addUpdateListener { animation ->
tabDot.setBackgroundColor(animation.animatedValue as Int)
}
colorAnimator.start()
}
Because it's replacing the background of the TabItem().
Solution is instead of setting the color, setting the same background with a different color, so you can preserve the edges and corners. I am going through the same work right now and I will edit a second time and provide you this code as well.
edit2:
Here's the working animateDotColor function for you, this one doesn't remove the corners. Enjoy it buddy.
private fun animateDotColor2(tab: TabView, color: Int) {
tab.background = AppCompatResources.getDrawable(tab.context, R.drawable.tablayout_selected_final)
}
Related
Need a solution to add a Shake View with a progress bar. This view is used as a Toast View in Android, I got a solution such that the View grows from the center and needs help in transforming into the expected result.
The current state is zipped in the drive with the actual video & expected video.
Video and Current state source code
I tried this growFromCenter Extension function, I even tried using Object animator but got blocked while combining it with the progress bar.
private fun View.growFromCenter(
duration: Long,
#FloatRange(from = 0.0, to = 1.0)
startScaleRatio: Float,
endAnimCallback: () -> Unit = {},
) {
val growFromCenter = ScaleAnimation(
startScaleRatio,
1f,
startScaleRatio,
1f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
)
growFromCenter.duration = duration
growFromCenter.fillAfter = true
growFromCenter.interpolator = FastOutSlowInInterpolator()
growFromCenter.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(p0: Animation?) {
}
override fun onAnimationEnd(p0: Animation?) {
endAnimCallback.invoke()
}
override fun onAnimationRepeat(p0: Animation?) {
}
})
startAnimation(growFromCenter)
}
Use SnackProgressBar instead, I think it is better and easier to manage.
Shake view with a progress bar
Step 1:
In res/anim create file animation.xml
<?xml version="1.0" encoding="utf-8"?>
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<scale
android:duration="350"
android:fromXScale="0.0"
android:fromYScale="0.5"
android:pivotX="100%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
<translate
android:duration="175"
android:fromXDelta="0%"
android:repeatCount="5"
android:repeatMode="reverse"
android:toXDelta="7%" />
</set>
Step 2: In your require Activity
class MainActivity : AppCompatActivity() {
private val viewModel:MainActivityViewModel by viewModels()
private lateinit var mBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding=ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
viewModel.currentTime.observe(this) {
val seconds = TimeUnit.MILLISECONDS.toSeconds(it*100)- TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(it*100)
)
val minutes = TimeUnit.MILLISECONDS.toMinutes(it*100)
Log.v("timmer", "$minutes : ${seconds}")
mBinding.progressBar.progress = it.toInt()
}
mBinding.btn.setOnClickListener {
mBinding.mtrlCardChecked.visibility= View.VISIBLE
mBinding.progressBar.visibility=View.VISIBLE
mBinding.mtrlCardChecked.startAnimation(AnimationUtils.loadAnimation(this, R.anim.animation))
viewModel.timer(15000L)
mBinding.progressBar.max=15*10
}
}}
Step 3: In ViewModel of your require activity
package com.example.toasty
import android.os.CountDownTimer
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MainActivityViewModel : ViewModel() {
private var ONE_SEC = 100L
lateinit var mCountDownTimer: CountDownTimer
private val _currentTime: MutableLiveData<Long> = MutableLiveData<Long>(0)
val currentTime: LiveData<Long> = _currentTime
fun timer(countTimer: Long) {
mCountDownTimer = object : CountDownTimer(countTimer, ONE_SEC) {
override fun onTick(p0: Long) {
_currentTime.value = (p0 / ONE_SEC)
}
override fun onFinish() {
}
}
mCountDownTimer.start()
}
}
Step 4: In your activity_main.xml file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/ctl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.button.MaterialButton
android:id="#+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Toasty!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/mtrl_card_checked"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="10dp"
android:visibility="gone"
app:cardCornerRadius="3dp"
app:cardElevation="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<View
android:id="#+id/v_bar"
android:layout_width="5dp"
android:layout_height="90dp"
android:background="#android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="#id/progressBar"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/iv_message_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="#android:color/holo_green_light"
android:src="#drawable/ic_launcher_foreground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/v_bar"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/iv_message_icon_type"
android:layout_width="20dp"
android:layout_height="20dp"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="#id/iv_message_icon"
app:layout_constraintEnd_toEndOf="#id/iv_message_icon" />
<TextView
android:id="#+id/tv_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="You have too many players in the same position. To pick another, transfer one out. (2/2)"
app:layout_constraintBottom_toBottomOf="#id/iv_message_icon"
app:layout_constraintEnd_toStartOf="#id/iBtn_message_popup_close"
app:layout_constraintStart_toEndOf="#id/iv_message_icon"
app:layout_constraintTop_toTopOf="#id/iv_message_icon" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/iBtn_message_popup_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="?actionBarItemBackground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="#+id/progressBar"
android:layout_width="0dp"
android:layout_height="50dp"
android:progress="10"
android:secondaryProgressTintMode="screen"
app:indicatorColor="#color/teal_200"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/v_bar"
app:trackColor="#color/purple_200"
app:trackThickness="10dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
You can directly download the sample project from Github repo
After making the Custom View, You can Use below code to show that with animation
val snackBarView = Snackbar.make(view, "SnackBar Message" , Snackbar.LENGTH_LONG)
val view = snackBarView.view
val params = view.layoutParams as FrameLayout.LayoutParams
params.gravity = Gravity.TOP
view.layoutParams = params
view.background = ContextCompat.getDrawable(context,R.drawable.custom_drawable) // for custom background
snackBarView.animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
snackBarView.show()
below line will resolve the animation issue.
snackBarView.animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
Alternate solution-
snackBarView.anchorView = mention viewId above whom you want to show SnackBar
In my current project I have a RecyclerView with a rounded rectangle as its border. I set it using the background view tag in my layout xml and it produces the correct effect:
<androidx.recyclerview.widget.RecyclerView
...
android:background="#drawable/layout_sample_view_background"
....
/>
The problem arises when I longclick() one of the view items and open a floating action mode menu using ActionMode.Callback2. This selection overlaps the borders of my RecyclerView:
Btw. Not sure this is important, but the viewitems inside my RecyclerView/ListAdapter are using a custom view:
/**
* Custom view for each [RecyclerView] list item in the list inside a [SampleView]
*/
class SampleListItem(context: Context, attrs: AttributeSet, #AttrRes defStyleAttr: Int) : ConstraintLayout(context, attrs, defStyleAttr), View.OnLongClickListener {
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
init {
setOnLongClickListener(this)
clipToOutline = false
}
private val binding: LayoutSampleListItemBinding = LayoutSampleListItemBinding.inflate(LayoutInflater.from(context), this)
var sample: Sample? = null
set(value) {
field = value
fun Long.toTimeString(): String {
val date = ZonedDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
val formatter = DateTimeFormatter.ofPattern("HH:mm")
val time = date.format(formatter)
Timber.d("With $this time was: $time")
return time
}
checkNotNull(value)
binding.id.text = value.sampleNumber.toString()
binding.timestamp.text = value.sampleTime.toTimeString()
binding.comment.text = SpannableStringBuilder(value.comment)
}
private var sampleItemClickListener = object : SampleItemClickListener {
override fun onSampleEditClick(sample: Sample) {
Timber.d("ActionMode edit icon clicked. Please edit $sample")
isSelected = !isSelected
}
override fun onSampleDeleteClick(sample: Sample) {
Timber.d("ActionMode delete icon clicked. Please delete $sample")
isSelected = !isSelected
}
}
interface SampleItemClickListener {
fun onSampleEditClick(sample: Sample)
fun onSampleDeleteClick(sample: Sample)
}
override fun onLongClick(v: View?): Boolean {
Toast.makeText(context,"longClick $sample", Toast.LENGTH_LONG).show()
// Start floating ActionMode
isSelected = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val floatingActionModeCallback = SampleViewItemActionModeCallback(this, sampleItemClickListener, R.menu.sampleviewitem_menu_actions)
}
return true
}
}
I set the ripple effect of this custom view using the following drawable:
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#color/colorPrimaryLight">
<item
android:id="#android:id/mask"
android:drawable="#color/icon_inactive_light_background" />
<item>
<selector>
<item android:state_selected="true">
<color android:color="#color/colorPrimaryLight"/>
</item>
<item android:state_activated="true">
<color android:color="#color/colorPrimaryLight"/>
</item>
<item>
<color android:color="#android:color/transparent"/>
</item>
</selector>
</item>
</ripple>
The layout of the custom view:
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<com.google.android.material.textview.MaterialTextView
android:id="#+id/id"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAlignment="center"
app:layout_constraintEnd_toStartOf="#+id/timestamp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/comment"
app:layout_constraintWidth_percent="0.10"
tools:text="#sample/samples.json/data/sampleId" />
<com.google.android.material.textview.MaterialTextView
android:id="#+id/timestamp"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAlignment="center"
app:layout_constraintEnd_toStartOf="#+id/comment"
app:layout_constraintStart_toEndOf="#+id/id"
app:layout_constraintTop_toTopOf="#+id/comment"
app:layout_constraintWidth_percent="0.20"
tools:text="#sample/samples.json/data/timestamp" />
<com.google.android.material.textview.MaterialTextView
android:id="#+id/comment"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#id/sampleviewitem_optional_icon"
app:layout_constraintStart_toEndOf="#id/timestamp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.60"
tools:hint="Kommentar"
tools:text="#sample/samples.json/data/comment" />
<com.google.android.material.button.MaterialButton
android:id="#+id/sampleviewitem_optional_icon"
style="#style/IconOnlyButton"
android:layout_width="0dp"
android:layout_height="0dp"
app:icon="#drawable/ic_edit_white_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="#+id/layout_sample_item_sample_comment"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintWidth_percent="0.10" />
</merge>
Does anyone know what causes this effect and how to avoid this behavior? I know of ViewOutlineProvider, but I'm just not familiar with it. Could it maybe solve this issue?
For others that might come across this problem. The solution to this was to wrap the RecyclerView in a MaterialCardView (a regular CardView could also work).
Like this:
<com.google.android.material.card.MaterialCardView
android:id="#+id/sample_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:cardElevation="0dp"
app:cardCornerRadius="4dp"
app:strokeColor="#color/icon_active_unfocused_light_background"
app:strokeWidth="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#id/layout_sample_view_icon"
app:layout_constraintTop_toTopOf="#id/layout_sample_view_icon"
>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/layout_sample_view_recyclerview"
android:layout_height="match_parent"
android:layout_width="match_parent"
tools:itemCount="4"
tools:listitem="#layout/layout_patientsample_listitem"
/>
</com.google.android.material.card.MaterialCardView>
Here's a screenshot of the outcome:
Use padding in your RecyclerView
<androidx.recyclerview.widget.RecyclerView
...
android:background="#drawable/layout_sample_view_background"
android:padding="5dp"
....
/>
In one of the fragments of the app i'm developing, i let the users create various chips and every chip represents an option. I was able to animate the chip creation.
Now, when the user taps on a chip, i remove it from the group. I was able to associate a custom animation to the removal (see the gif) but when a "middle chip" is deleted, the chips to the right suddenly move to the left, when the animation is over.
Layout:
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="#dimen/horizontal_scroll_height"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:id="#+id/rouletteChipList"
style="#style/Widget.MaterialComponents.ChipGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="#dimen/chip_horizontal_margin"
android:paddingEnd="#dimen/chip_horizontal_margin"
app:chipSpacing="#dimen/chip_spacing"
app:singleLine="true">
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
Chip deletion:
private void removeChip(final Chip chip) {
#SuppressWarnings("ConstantConditions") final ChipGroup optionsList = getView().findViewById(R.id.ChipList);
// Remove the chip with an animation
if (chip == null) return;
final Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.chip_exit_anim);
chip.startAnimation(animation);
chip.postDelayed(new Runnable() {
#Override
public void run() {
optionsList.removeView(chip);
}
}, 400);
}
Chip layout:
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/placeholder"
android:textAlignment="center"
app:chipBackgroundColor="#color/grayTranslucent"
app:rippleColor="?colorAccent" />
I'd like to have a smooth animation, where the chips smoothly move to the left when a "middle chip" is deleted. I tried a couple of things, but no luck.
if you mean something like this
i,ve added it inside a HSV and added android:animateLayoutChanges="true" on chip_group, see below code
<HorizontalScrollView
android:scrollbars="none"
.
>
<com.google.android.material.chip.ChipGroup
android:id="#+id/chip_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
inside my Code i have added chips dynamically to this chip_group.
val newChip = Chip(this, null, R.attr.chipStyle)
newChip.text = "text"
newChip.isClickable = true
newChip.isFocusable = true
newChip.setOnClickListener(chipClickListener)
chip_group.addView(newChip)
and there is a onClickListener on each chip.inside it i start a fade animation and in onAnimationEnd do a removeView on chip_group
private val chipClickListener = View.OnClickListener {
val anim = AlphaAnimation(1f,0f)
anim.duration = 250
anim.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
chip_group.removeView(it)
}
override fun onAnimationStart(animation: Animation?) {}
})
it.startAnimation(anim)
}
I have a BottomSheet which houses a product detail card. The problem is, when I click on the + or - button on the product detail while the bottom sheet is in it's Expanded state, it jumps down.
When it is down and I click on the buttons it doesn't jump, it only happens when it is in it's Expanded (completely up) state
I have attached a GIF to show what is exactly happening
Here is the code
scan_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:animateLayoutChanges="false"
android:background="#drawable/bottom_sheet_dialog_fragment"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="100dp"
app:layout_behavior="studio.monotype.storedemo.BottomSheetBehavior">
<include
layout="#layout/hero_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/divider_view"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginStart="24dp"
android:layout_marginTop="44dp"
android:layout_marginEnd="24dp"
android:background="#color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/hero_item" />
<include
layout="#layout/related_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#+id/divider_view"
tools:layout_editor_absoluteX="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
ScanActivity.kt (simplified to show only what is essential)
class ScanActivity : AppCompatActivity() {
private lateinit var bottomSheet: BottomSheetBehavior<*>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scan)
setupBottomSheet()
showSheet()
}
private fun setupBottomSheet() {
bottomSheet = BottomSheetBehavior.from(bottom_sheet)
bottomSheet.isHideable = true
bottomSheet.skipCollapsed= true
bottomSheet.isDraggable = true
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
bottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
#SuppressLint("SwitchIntDef")
override fun onStateChanged(sheet: View, newState: Int) {
when (newState) {
BottomSheetBehavior.STATE_HIDDEN -> {
codeScanner.startPreview()
}
}
}
})
plus_btn.setOnClickListener {
var qty= qty_tv.text.toString().toInt()
qty++
qty_tv.text =qty.toString()
}
minus_btn.setOnClickListener {
var qty= qty_tv.text.toString().toInt()
if(qty!=0)
{
qty--
}
qty_tv.text =qty.toString()
}
}
private fun showSheet() {
bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
}
}
it seems that google engineer gave correct answer
Seems like something is going on because you are setting
android:layout_gravity="bottom" on the view with the
BottomSheetBehavior. You should remove that line.
It helped on my case
Looks to me like that could be a bug in the BottomSheetBehavior? Seems like the height of the sheet isn't getting saved or restored correctly. After the button is pressed, a layout happens again which changes the height. Could you file a bug at https://issuetracker.google.com/issues/new?component=439535
I am trying to add a badge to the BottomNavigationView Item without using any library, however somehow the BottomNavigationView is not showing the badge (custom_view)
main_view.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.hrskrs.test.MainActivity">
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:itemBackground="#color/colorPrimary"
app:itemIconTint="#color/colorAccent"
app:itemTextColor="#color/colorPrimaryDark"
app:menu="#menu/bottom_navigation_main" />
</RelativeLayout>
bottom_navigation_menu.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/item_test"
android:icon="#mipmap/ic_launcher"
android:title="action1"
app:showAsAction="always" />
<item
android:enabled="true"
android:icon="#mipmap/ic_launcher"
android:title="action2"
app:showAsAction="ifRoom" />
<item
android:enabled="true"
android:icon="#mipmap/ic_launcher"
android:title="action3"
app:showAsAction="ifRoom" />
</menu>
Activity extended from AppCompatActivity:
#Override
public boolean onCreateOptionsMenu(Menu menu) {
menu = bottomNavigationView.getMenu();
menu.clear();
getMenuInflater().inflate(R.menu.bottom_navigation_main, menu);
MenuItem item = menu.findItem(R.id.item_test);
item = MenuItemCompat.setActionView(item, R.layout.custom_view);
RelativeLayout badgeWrapper = (RelativeLayout) MenuItemCompat.getActionView(item);
TextView textView = (TextView) badgeWrapper.findViewById(R.id.txtCount);
textView.setText("99+");
return super.onCreateOptionsMenu(menu);
}
custom_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="#android:style/Widget.ActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:clickable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Notification Icon"
android:gravity="center"
android:src="#mipmap/ic_launcher" />
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/txtCount"
android:gravity="right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/ic_badge"
android:text="55"
android:textColor="#ffffffff"
android:textSize="12sp" />
</RelativeLayout>
Istead of showing (badge) custom_view it shows the item itself only:
Below you can see from the debug mode that the view accessed is the right one and it is being set correctly. However somehow the BottomNavigationViewis not being invalidated:
You can use the BottomNavigationView provided by the Material Components Library.
Just add the BottomNavigationView to your layout:
<com.google.android.material.bottomnavigation.BottomNavigationView
android:layout_gravity="bottom"
app:menu="#menu/navigation_main"
../>
Then use in your code:
int menuItemId = bottomNavigationView.getMenu().getItem(0).getItemId();
BadgeDrawable badge = bottomNavigationView.getOrCreateBadge(menuItemId);
badge.setNumber(2);
To change the badge gravity use the setBadgeGravity method.
badge.setBadgeGravity(BadgeDrawable.BOTTOM_END);
I managed to make BottomNavigationView with the badge. Here is my code (Kotlin).
This is the panel (inherited from BottomNavigationView)
/** Bottom menu with badge */
class AdvancedBottomNavigationView(context: Context, attrs: AttributeSet) : BottomNavigationView(context, attrs) {
private companion object {
const val BADGE_MIN_WIDTH = 16 // [dp]
const val BADGE_MARGIN_TOP = 5 // [dp]
const val BADGE_MARGIN_LEFT = 15 // [dp]
}
#Inject internal lateinit var uiCalculator: UICalculatorInterface
private val bottomMenuView: BottomNavigationMenuView
init {
// Get access to internal menu
val field = BottomNavigationView::class.java.getDeclaredField("mMenuView")
field.isAccessible = true
bottomMenuView = field.get(this) as BottomNavigationMenuView
App.injections.presentationLayerComponent!!.inject(this)
#SuppressLint("CustomViewStyleable")
val a = context.obtainStyledAttributes(attrs, R.styleable.advanced_bottom_navigation_bar)
val badgeLayoutId = a.getResourceId(R.styleable.advanced_bottom_navigation_bar_badge_layout, -1)
a.recycle()
initBadges(badgeLayoutId)
}
/**
* [position] index of menu item */
fun setBadgeValue(position: Int, count: Int) {
val menuView = bottomMenuView
val menuItem = menuView.getChildAt(position) as BottomNavigationItemView
val badge = menuItem.findViewById(R.id.bottom_bar_badge)
val badgeText = menuItem.findViewById(R.id.bottom_bar_badge_text) as TextView
if (count > 0) {
badgeText.text = count.toString()
badge.visibility = View.VISIBLE
} else {
badge.visibility = View.GONE
}
}
/**
* Select menu item
* [position] index of menu item to select
*/
fun setSelected(position: Int) = bottomMenuView.getChildAt(position).performClick()
private fun initBadges(badgeLayoutId: Int) {
// Adding badges to each Item
val menuView = bottomMenuView
val totalItems = menuView.childCount
val oneItemAreaWidth = uiCalculator.getScreenSize(context).first / totalItems
val marginTop = uiCalculator.dpToPixels(context, BADGE_MARGIN_TOP)
val marginLeft = uiCalculator.dpToPixels(context, BADGE_MARGIN_LEFT)
for (i in 0 until totalItems) {
val menuItem = menuView.getChildAt(i) as BottomNavigationItemView
// Add badge to every item
val badge = View.inflate(context, badgeLayoutId, null) as FrameLayout
badge.visibility = View.GONE
badge.minimumWidth = uiCalculator.dpToPixels(context, BADGE_MIN_WIDTH)
val layoutParam = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT)
layoutParam.gravity = Gravity.START
layoutParam.setMargins(oneItemAreaWidth / 2 + marginLeft, marginTop, 0, 0)
menuItem.addView(badge, layoutParam)
}
}
}
It's attr.xml file with options for this component:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="advanced_bottom_navigation_bar">
<attr name="badge_layout" format="reference|integer" />
</declare-styleable>
</resources>
Background for badge from drawable folder:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#ff0000" />
<corners android:radius="10dp" />
</shape>
</item>
</selector>
Badge itself:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="#+id/bottom_bar_badge"
android:layout_height="20dp"
android:layout_width="20dp"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#drawable/bcg_badge"
>
<TextView
android:id="#+id/bottom_bar_badge_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textSize="10sp"
android:textColor="#android:color/white"
xmlns:android="http://schemas.android.com/apk/res/android"
android:textAlignment="center"
android:layout_gravity="center"/>
</FrameLayout>
And this is an example how to use it in your code:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="su.ivcs.ucim.presentationLayer.userStories.mainScreen.view.MainActivity">
<su.ivcs.ucim.presentationLayer.common.advancedBottomNavigationView.AdvancedBottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:itemBackground="#android:color/white"
app:itemIconTint="#color/main_screen_tabs_menu_items"
app:itemTextColor="#color/main_screen_tabs_menu_items"
app:menu="#menu/main_screen_tabs_menu"
app:badge_layout = "#layout/common_badge"
app:layout_constraintTop_toBottomOf="#+id/fragmentsContainer"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</android.support.constraint.ConstraintLayout>
I hope this helps you.
#hrskrs Try adding a higher elevation on your txtCount or badgeWrapper itself.
BottomNavigationView seems to have higher elevation than the views on the screen.
I struggled with showing badges on BottomNavigationView items. My badge (without any text value) being part of the drawable itself turned grey when user clicked other item or became the same color defined in the tint (if not defined is colorPrimary).
I think you will run into the same problem I faced with colouring of the badge/counter on top of menu item of BottomNavigationViewas tint color will be applied to the item itself and your badgeWrapper being part of MenuItem will take the tint (turns grey when you tap any other item which you will not want I guess).
Check out my answer here: Is there a way to display notification badge on Google's official BottomNavigationView menu items introduced in API 25?
I used an ImageView for a badge but you can have your badgeWrapper RelativeView instead of the ImageView.
use the BadgeDrawable like this:
Integer amount = tabBadgeCountMap.get(tab);
BadgeDrawable badgeDrawable = bottomNavigationView.getOrCreateBadge(tabMenuResId);
badgeDrawable.setNumber(amount != null ? amount : 0);