This question already has answers here:
Badge on BottomNavigationView
(4 answers)
Closed 2 years ago.
I want to set indicator on bottomNavigationView.You can see that orange line at bottom side
How to set indicator?
I didn't find any example for indicator.
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#drawable/bottom_navigation_bg"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_menu_main"/>
My previous answer offers a lot of flexibility and animates nicely between selected states but involves a relatively large amount of code. A simpler solution that comes without animation, would be to use the itemBackground attribute of the BottomNavigationView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemBackground="#drawable/bottom_nav_tab_background"
android:layout_gravity="bottom"
app:menu="#menu/menu_bottom_nav" />
</LinearLayout>
bottom_nav_tab_background
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item android:gravity="bottom">
<shape android:shape="rectangle">
<size android:height="4dp" />
<solid android:color="?attr/colorPrimary" />
</shape>
</item>
</layer-list>
</item>
</selector>
Here's something I whipped up to add an indicator and move it around.
class IndicatorBottomNavigationView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.bottomNavigationStyle
) : BottomNavigationView(context, attrs, defStyleAttr) {
private val indicator: View = View(context).apply {
layoutParams = LayoutParams(0, 5)
setBackgroundColor(ContextCompat.getColor(context, R.color.carecredit_green))
}
init {
addView(indicator)
}
var animateIndicator = true
override fun setOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener?) {
OnNavigationItemSelectedListener { selectedItem ->
menu
.children
.first { item ->
item.itemId == selectedItem.itemId
}
.itemId
.let {
findViewById<View>(it)
}
.let { view ->
this.post {
indicator.layoutParams = LayoutParams(view.width, 5)
if (animateIndicator) {
indicator
.animate()
.x(view.x)
.start()
} else {
indicator.x = view.x
}
indicator.y = view.y
}
}
listener?.onNavigationItemSelected(selectedItem) ?: false
}.let {
super.setOnNavigationItemSelectedListener(it)
}
}
}
To get this working you need to extend the BottomNavigationView to be able to have multiple listeners. The original implementation only allows for one but you need another to be able to tell the indicator when to move.
class CustomBottomNavigationView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BottomNavigationView(context, attrs, defStyleAttr), OnNavigationItemSelectedListener {
private val onNavigationItemSelectedListeners =
mutableListOf<OnNavigationItemSelectedListener>()
init {
super.setOnNavigationItemSelectedListener(this)
itemIconTintList = null
}
override fun setOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener?) {
if (listener != null) addOnNavigationItemSelectedListener(listener)
}
fun addOnNavigationItemSelectedListener(listener: OnNavigationItemSelectedListener) {
onNavigationItemSelectedListeners.add(listener)
}
fun addOnNavigationItemSelectedListener(listener: (Int) -> Unit) {
addOnNavigationItemSelectedListener(OnNavigationItemSelectedListener {
for (i in 0 until menu.size()) if (menu.getItem(i) == it) listener(i)
false
})
}
override fun onNavigationItemSelected(item: MenuItem) = onNavigationItemSelectedListeners
.map { it.onNavigationItemSelected(item) }
.fold(false) { acc, it -> acc || it }
}
Then create an indicator item:
class BottomNavigationViewIndicator #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
val size = 8f
private val targetId: Int
private var target: BottomNavigationMenuView? = null
private var rect = Rect()
// val drawPaint = Paint()
private val backgroundDrawable: Drawable
private var index = 0
private var animator: AnimatorSet? = null
init {
if (attrs == null) {
targetId = NO_ID
backgroundDrawable = ColorDrawable(Color.TRANSPARENT)
} else {
with(context.obtainStyledAttributes(attrs, BottomNavigationViewIndicator)) {
targetId =
getResourceId(BottomNavigationViewIndicator_targetBottomNavigation, NO_ID)
val clippableId =
getResourceId(BottomNavigationViewIndicator_clippableBackground, NO_ID)
backgroundDrawable = if (clippableId != NO_ID) {
androidx.appcompat.content.res.AppCompatResources.getDrawable(
context,
clippableId
) ?: ColorDrawable(Color.TRANSPARENT)
} else {
ColorDrawable(
getColor(
BottomNavigationViewIndicator_clippableBackground,
Color.TRANSPARENT
)
)
}
recycle()
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (targetId == NO_ID) return attachedError("invalid target id $targetId, did you set the app:targetBottomNavigation attribute?")
val parentView =
parent as? View ?: return attachedError("Impossible to find the view using $parent")
val child = parentView.findViewById<View?>(targetId)
if (child !is CustomBottomNavigationView) return attachedError("Invalid view $child, the app:targetBottomNavigation has to be n ListenableBottomNavigationView")
for (i in 0 until child.childCount) {
val subView = child.getChildAt(i)
if (subView is BottomNavigationMenuView) target = subView
}
elevation = child.elevation
child.addOnNavigationItemSelectedListener { updateRectByIndex(it, true) }
post { updateRectByIndex(index, false) }
}
private fun attachedError(message: String) {
Log.e("BNVIndicator", message)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
target = null
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.clipRect(rect)
// canvas.drawCircle(size,size,size, drawPaint)
backgroundDrawable.draw(canvas)
}
private fun updateRectByIndex(index: Int, animated: Boolean) {
this.index = index
target?.apply {
if (childCount < 1 || index >= childCount) return
val reference = getChildAt(index)
val start = reference.left + left
val end = reference.right + left
backgroundDrawable.setBounds(left, top, right, bottom)
val newRect = Rect(start, 0, end, height)
if (animated) startUpdateRectAnimation(newRect) else updateRect(newRect)
}
}
private fun startUpdateRectAnimation(rect: Rect) {
animator?.cancel()
animator = AnimatorSet().also {
it.playTogether(
ofInt(this, "rectLeft", this.rect.left, rect.left),
ofInt(this, "rectRight", this.rect.right, rect.right),
ofInt(this, "rectTop", this.rect.top, rect.top),
ofInt(this, "rectBottom", this.rect.bottom, rect.bottom)
)
it.interpolator = FastOutSlowInInterpolator()
it.duration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
it.start()
}
}
private fun updateRect(rect: Rect) {
this.rect = rect
postInvalidate()
}
#Keep
fun setRectLeft(left: Int) = updateRect(rect.apply { this.left = left })
#Keep
fun setRectRight(right: Int) = updateRect(rect.apply { this.right = right })
#Keep
fun setRectTop(top: Int) = updateRect(rect.apply { this.top = top })
#Keep
fun setRectBottom(bottom: Int) = updateRect(rect.apply { this.bottom = bottom })
}
Put some attributes into attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BottomNavigationViewIndicator">
<attr name="targetBottomNavigation" format="reference"/>
<attr name="clippableBackground" format="reference|color"/>
</declare-styleable>
</resources>
Create a background drawable that'll be clipped as the indicator moves:
my_orange_background.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="24dp"
android:height="24dp"
android:viewportWidth="360"
android:viewportHeight="4">
<path
android:fillType="nonZero"
android:pathData="M0,0h359.899v3.933H0z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="360"
android:endY="2"
android:startX="0"
android:startY="2"
android:type="linear">
<item
android:color="#color/orange"
android:offset="0" />
<item
android:color="#color/orange"
android:offset="0.55" />
</gradient>
</aapt:attr>
</path>
</vector>
Create your activity/ fragment layout:
<?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"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="#+id/bottom_nav"
app:layout_constraintTop_toTopOf="parent" />
<com.example.app.view.CustomBottomNavigationView
android:id="#+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?attr/colorSurface"
app:labelVisibilityMode="unlabeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="#menu/menu_bottom_nav" />
<com.example.app.view.BottomNavigationViewIndicator
android:layout_width="0dp"
android:layout_height="4dp"
app:clippableBackground="#drawable/my_orange_background"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="#+id/bottom_nav"
app:targetBottomNavigation="#+id/bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
Sources: Medium article GitHub project
It is bad practice but if you wish to do like this you can take Tab layout.
here is the code
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/tabs"
/>
<android.support.design.widget.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:tabBackground="#color/colorPrimary"
app:tabIndicatorColor="#android:color/white"
app:tabMode="fixed"
app:tabGravity="fill"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
Related
I have some missunderstading why this is happening. I have a parent FrameLayout that has rounded corners, and a TextView with bottom rounded corners. Corner raduses are equal, but on the screen displaying some extra pixels.
I got the same behavior with default xml rounding, and i would like to receive right work of rounded corners with not affect of parent view.
Custom text View
class SampleTextView #JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attributeSet, defStyleAttr) {
private val cornerRadius: Float = 20f
init {
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val left = 0
val top = 0
val right = view.width
val bottom = view.height
outline.setRoundRect(
left,
(top - cornerRadius).toInt(),
right,
bottom,
cornerRadius
)
}
}
clipToOutline = true
}
}
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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/black"
tools:context=".MainActivity">
<FrameLayout
android:id="#+id/fl"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.example.testproject.SampleTextView
android:id="#+id/child"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="bottom"
android:background="#color/black"
android:text="Some text text"
android:textColor="#color/white" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val parent = findViewById<FrameLayout>(R.id.fl)
parent.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val left = 0
val top = 0
val right = view.width
val bottom = view.height
outline.setRoundRect(left, top, right, bottom, 20.toFloat())
}
}
parent.clipToOutline = true
}
}
Strange behavior
I have custom LoadingButton class implemented as FrameLayout which is then used inside XMLs as component. Its basically Button but made completely custom with its own layout and components.
What I want is to add shadow there (elevation, translationZ) but this shadow is clipped everywhere.
I want to have this button dynamic that I can adjust its margins or change its shape like adding static width and height on different screens without distorting shadow around. Shadow is clipped either from top or bottom all the time.
Example of xml view:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="CustomRes" type="com.project.utils.CustomResources"/>
</data>
<merge
android:duplicateParentState="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:id="#+id/buttonParent"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:duplicateParentState="true"
android:clipToPadding="false"
android:clipChildren="false"
android:gravity="center">
<ImageView
android:id="#+id/buttonIcon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:layout_gravity="center"
android:scaleType="fitCenter"/>
<TextView
android:id="#+id/buttonText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:textSize="#dimen/text_medium"
android:textStyle="bold"
android:lines="1"
android:layout_gravity="center"/>
</LinearLayout>
<com.project.components.loading_indicator.LoadingIndicator
android:id="#+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" />
</merge>
</layout>
Class:
#Suppress("DEPRECATION")
class LoadingButton : FrameLayout {
#StyleableRes
internal val btnIcoIndex = 0
#SuppressLint("ResourceType")
#StyleableRes
internal val btnTextResIndex = 1
#StyleableRes
#SuppressLint("ResourceType")
internal val btnTextIndex = 2
#StyleableRes
#SuppressLint("ResourceType")
internal val btnTextSizeIndex = 3
private val buttonParent: LinearLayout
private val progressBar: LoadingIndicator
private val buttonIcon: ImageView
private val buttonText: TextView
private var buttonTextVal: String? = null
init {
CustomResources.inflateLayout(LayoutInflater.from(context), R.layout.loading_button, this)
buttonParent = findViewById(R.id.buttonParent)
buttonIcon = findViewById(R.id.buttonIcon)
buttonText = findViewById(R.id.buttonText)
progressBar = findViewById(R.id.progress)
buttonText.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
textViewInitWidth = maxOf(v.measuredWidth, textViewInitWidth)
}
}
#SuppressLint("ClickableViewAccessibility")
constructor(context: Context) : super(context) {
parseAttrs(context)
}
#SuppressLint("ClickableViewAccessibility")
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
parseAttrs(context, attrs)
}
#SuppressLint("ClickableViewAccessibility")
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
parseAttrs(context, attrs, defStyleAttr)
}
#SuppressLint("ResourceType")
private fun parseAttrs(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int? = null) {
//Load from custom attributes
setLoading(false)
val sets = intArrayOf(R.attr.l_buttonIcon, R.attr.l_buttonTextId, R.attr.l_buttonText, R.attr.l_buttonTextSize)
if (attrs != null){
val typedArray = context.obtainStyledAttributes(attrs, sets)
val buttonIco = typedArray.getResourceId(btnIcoIndex, 0)
val buttonTxtRes = typedArray.getText(btnTextResIndex)?.let { res->
App.getString(res as String).toLowerCase(Locale.getDefault()).replaceFirstChar { it.toUpperCase() }
}
val buttonTxtRaw = typedArray.getText(btnTextIndex)?.let { it as String }?:"null"
val buttonTxtSize = typedArray.getDimension(btnTextSizeIndex, resources.getDimension(R.dimen.text_medium))
val buttonStyle = attrs.styleAttribute
App.log("RawTextButton: $buttonTxtRaw, hasValue: ${typedArray.getString(btnTextIndex)}")
setButtonTextSize(buttonTxtSize)
buttonTxtRes?.let { setButtonText(buttonTxtRes) }?:kotlin.run{ setButtonText(buttonTxtRaw) }
setButtonIcon(buttonIco)
buttonParent.gravity = Gravity.CENTER
buttonTextVal = buttonTxtRes?.let { buttonTxtRes.toString() }?:buttonTxtRaw
typedArray.recycle()
clipToPadding = false
clipChildren = false
when(buttonStyle){
R.style.button_primary -> {
App.log("BtnStyleId - primary")
setupTextStyle(buttonStyle)
setProgBarColor(R.color.button_light)
setIconTint(ContextCompat.getColor(context, R.color.button_light))
maybeSetAmbientShadow(R.color.button_primary)
}
R.style.button_secondary -> {
App.log("BtnStyleId - secondary")
setupTextStyle(buttonStyle)
setProgBarColor(R.color.button_primary)
setIconTint(ContextCompat.getColor(context, R.color.button_light))
maybeSetAmbientShadow(R.color.button_primary)
}
R.style.button_secondary_alert -> {
App.log("BtnStyleId - secondary alert")
setupTextStyle(buttonStyle)
setProgBarColor(R.color.button_invalid)
setIconTint(ContextCompat.getColor(context, R.color.button_invalid))
maybeSetAmbientShadow(R.color.button_invalid)
}
else -> App.log("BtnStyleId -> $buttonStyle")
}
} else {
App.log("BtnStyleId -> attrs==null")
}
}
#SuppressLint("ResourceType")
private fun setupTextStyle(buttonStyle: Int){
val attrs = intArrayOf(android.R.attr.textAppearance)
val typedAttrs = context.obtainStyledAttributes(buttonStyle, attrs)
val textAppearance = typedAttrs.getResourceId(0, 0)
TextViewCompat.setTextAppearance(buttonText, textAppearance)
typedAttrs.recycle()
}
private fun setProgBarColor(color: Int){
progressBar.setColorTint(color)
}
fun setIconTint(color: Int){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
buttonIcon.colorFilter = BlendModeColorFilter(color, BlendMode.SRC_IN)
} else {
App.log("setting color filter")
buttonIcon.setColorFilter(color, PorterDuff.Mode.SRC_IN)
}
}
private fun maybeSetAmbientShadow(color: Int){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
outlineAmbientShadowColor = ContextCompat.getColor(this.context, color)
outlineSpotShadowColor = ContextCompat.getColor(this.context, color)
}
}
}
I need some variable to set which will set clipToPadding false and clipChildren false for every single screen where this button is implemented without rewriting 80+ xml files. Because some screens have marginTop set for this button but not marginBottom, and that will cause clipping even if I set those 2 parameters to false. Clearly bad design from Android for handling shadows.
Base style for primary and secondary button:
<style name="button" parent="#android:style/Widget.Material.Button">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:minHeight">0dp</item>
<item name="android:minWidth">0dp</item>
<item name="android:paddingStart">#dimen/button_padding_start</item>
<item name="android:paddingEnd">#dimen/button_padding_end</item>
<item name="android:paddingTop">#dimen/button_padding_top</item>
<item name="android:paddingBottom">#dimen/button_padding_bottom</item>
<item name="android:elevation">4dp</item>
<item name="android:translationZ">4dp</item>
<item name="android:stateListAnimator">#null</item>
<item name="android:clipToPadding">false</item>
</style>
Usage:
<com.project.components.loading_button.LoadingButton
android:id="#+id/loginButton"
android:layout_gravity="center"
android:layout_weight="0.5"
android:layout_marginStart="8dp"
app:l_buttonTextId="button_login"
style="#style/button.primary" />
When I'm putting my TabLayout inside MaterialCardView to make TabLayout rounder but I'm not getting desired result
<?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"
android:layout_width = "match_parent"
android:layout_height = "match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:cardCornerRadius="#dimen/dp_20"
android:theme="#style/Theme.MaterialComponents.Light"
app:strokeColor="#color/dark_blue"
app:cardPreventCornerOverlap="true"
app:strokeWidth="1dp">
<com.google.android.material.tabs.TabLayout
android:id = "#+id/tab_layout"
android:layout_width = "match_parent"
android:layout_height = "#dimen/dp_35"
app:tabGravity = "fill"
app:tabIndicatorColor = "#color/dark_blue"
app:tabIndicatorGravity = "stretch"
app:tabMaxWidth = "0dp"
app:tabMode = "fixed"
app:tabSelectedTextColor = "#android:color/white"
app:tabTextAppearance = "#style/AppTabTextTools"
app:tabTextColor = "?attr/colorPrimary">
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Please help me to figure out what's the problem in above layout.
I need result like below image
As suggested by #Malik Saifullah followed https://stackoverflow.com/a/50621395/770703
Created custom RoundedTabLayout class for simplification
class RoundedTabLayout : TabLayout, TabLayout.OnTabSelectedListener {
private lateinit var currentContext: Context
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
currentContext = context
addOnTabSelectedListener(this)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs, 0) {
currentContext = context
addOnTabSelectedListener(this)
}
constructor(context: Context) : super(context)
override fun onTabSelected(tab: Tab?) {
if (selectedTabPosition == 0) {
setTabBG(R.drawable.tab_left_select, R.drawable.tab_right_unselect)
} else {
setTabBG(R.drawable.tab_left_unselect, R.drawable.tab_right_select)
}
}
override fun onTabUnselected(tab: Tab?) {
}
override fun onTabReselected(tab: Tab?) {
}
private fun setTabBG(tab1: Int, tab2: Int) {
val tabStrip = getChildAt(0) as ViewGroup
val tabView1 = tabStrip.getChildAt(0)
val tabView2 = tabStrip.getChildAt(1)
if (tabView1 != null) {
val paddingStart = tabView1.paddingStart
val paddingTop = tabView1.paddingTop
val paddingEnd = tabView1.paddingEnd
val paddingBottom = tabView1.paddingBottom
ViewCompat.setBackground(tabView1, AppCompatResources.getDrawable(tabView1.context, tab1))
ViewCompat.setPaddingRelative(tabView1, paddingStart, paddingTop, paddingEnd, paddingBottom)
}
if (tabView2 != null) {
val paddingStart = tabView2.paddingStart
val paddingTop = tabView2.paddingTop
val paddingEnd = tabView2.paddingEnd
val paddingBottom = tabView2.paddingBottom
ViewCompat.setBackground(tabView2, AppCompatResources.getDrawable(tabView2.context, tab2))
ViewCompat.setPaddingRelative(tabView2, paddingStart, paddingTop, paddingEnd, paddingBottom)
}
}
}
Custom view:
class BarView : View {
private var mBarPaint = Paint(ANTI_ALIAS_FLAG)
private var mBarWidthRatio = 0f
private var mBarColor = Color.BLACK
var barWidthRatio
get() = mBarWidthRatio
set(value) {
mBarWidthRatio = value
invalidate()
}
var barColor
get() = mBarColor
set(value) {
mBarColor = value
invalidate()
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
context.obtainStyledAttributes(attrs, R.styleable.BarView).apply {
mBarWidthRatio = getFloat(R.styleable.BarView_barColor, 0f)
mBarColor = getColor(R.styleable.BarView_barWidthRatio, 0)
recycle()
}
}
override fun onDraw(canvas: Canvas) {
mBarPaint.color = mBarColor
canvas.drawRect(0f, 0f, width * mBarWidthRatio, height.toFloat(), mBarPaint)
}
}
Styleable resource:
<resources>
<declare-styleable name="BarView">
<attr name="barColor" format="color" />
<attr name="barWidthRatio" format="float" />
</declare-styleable>
</resources>
Activity 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/myLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="134dp" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.barchart2.BarView
android:id="#+id/bar"
android:layout_width="200dp"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="#+id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:barWidthRatio="0.75" />
</androidx.constraintlayout.widget.ConstraintLayout>
Activity class:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bar.setBackgroundColor(-10000)
button.setOnClickListener() {
println("Width: " + bar.width)
println("Height: " + bar.height)
println("Left: " + bar.left)
println("Top: " + bar.top)
println("Right: " + bar.right)
println("Bottom: " + bar.bottom)
}
}
}
I get the following error:
"Unable to start activity ComponentInfo{com.example.barchart2/com.example.barchart2.MainActivity}: android.view.InflateException: Binary XML file line #28: Binary XML file line #28: Error inflating class com.example.barchart2.BarView"
It was working with integer or string resources, but not float or fraction.
Found my mistake:
mBarWidthRatio = getFloat(R.styleable.BarView_barColor, 0f)
mBarColor = getColor(R.styleable.BarView_barWidthRatio, 0)
Mixed up the color and ratio.
I have checked many answers to find my issue however I was not successful. I have an activity that holds a compound drawable.
<layout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.my.profile.widgets.ProfileWidget
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
....
</LinearLayout>
</layout>
This is my ProfileWidget:
class ProfileWidget #JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
#Inject lateinit var viewModel: ProfileWidgetViewData
#Inject lateinit var viewActions: ProfileWidgetActions
private val binding: WidgetProfileBinding = DataBindingUtil.inflate(
LayoutInflater.from(context), R.layout.widget_profile, this, true)
// private val binding = WidgetProfileBinding.inflate(LayoutInflater.from(context), this, true)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setupDependencyInjection()
setupDataBinding()
viewActions.testUI()
}
private fun setupDependencyInjection() {
(context as ProfileActivity).getProfileComponent()?.inject(this)
}
private fun setupDataBinding() {
binding.viewModel = viewModel
}
}
This is its layout:
<layout 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">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.my.profile.widgets.ProfileWidgetViewData" />
</data>
<LinearLayout
android:id="#+id/profilesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#FF0000"
>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="profile 1"
android:visibility="#{viewModel.textView_1.get() ? View.VISIBLE : View.INVISIBLE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="profile 2"
android:visibility="#{viewModel.textView_2.get() ? View.VISIBLE : View.INVISIBLE}"/>
</LinearLayout>
</layout>
Finally my ViewModel class supposed to make TextViews
visible/invisible.
interface ProfileWidgetViewData {
val textView_1: ObservableBoolean
val textView_2: ObservableBoolean
}
interface ProfileWidgetActions {
fun testUI()
}
class ProfileWidgetViewModelImpl : ProfileWidgetViewData, ProfileWidgetActions {
override val textView_1 = ObservableBoolean(false)
override val textView_2 = ObservableBoolean(false)
override fun testUI() {
setProfilesContainerVisibility(true)
setAddProfileContainerVisibility(true)
}
private fun setProfilesContainerVisibility(isVisible: Boolean) {
textView_1.set(isVisible)
}
private fun setAddProfileContainerVisibility(isVisible: Boolean) {
textView_2.set(isVisible)
}
}
Unfortunately I don't see anything wrong in above codes. When I launch
the app, those two TextView are Invisible although I have set them to be visible.
Check below is added or not in build.gradle(obviously you already added)
apply plugin: 'kotlin-kapt'
android {
dataBinding {
enabled = true
}
}
dependencies {
kapt "com.android.databinding:compiler:3.1.3"
}
And add below line in your xml file for visibility or invisible
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="profile 1"
android:visibility="#{safeUnbox(viewModel.textView_1) ? View.VISIBLE : View.INVISIBLE}"/>