How to set values for AbstractComposeView using XML? - android

I have a Composable wrapped in AbstractComposeView to be used in XML.
How to set values to this view.
Example,
Composable and AbstractComposeView
class MyComposeView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AbstractComposeView(context, attrs, defStyleAttr) {
private var titleText by mutableStateOf("Default text")
var titleValue: String
get() = titleText
set(value) {
titleText = value
}
#Composable
override fun Content() {
ListItem(
text = titleText,
)
}
}
#Composable
fun ListItem(
text: String,
) {
Row(
modifier = Modifier,
) {
Text(
text = text,
)
}
}
XML
<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"
tools:context=".ViewActivity">
<com.example.android.MyComposeView
android:id="#+id/my_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Activity
findViewById<MyComposeView>(R.id.my_view).titleValue = "From Activity"
This gives the intended result, but how to achieve the same without any code changes in the activity?
Something like app:titleText? (Tried this didn't work).

As for a regular custom view, you will need to rely on attribut set.
First create it in res > values > attrs.xml
Somethings like:
<resources>
<declare-styleable name="MyComposeView">
<attr name="titleText" format="string" />
</declare-styleable>
</resources>
Then apply it in your MyComposeView class
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.MyComposeView,
0, 0).apply {
try {
titleText = attributes.getString(R.styleable.MyComposeView_ titleText).toString()
} finally {
recycle()
}
}
}
Références:
https://developer.android.com/develop/ui/views/layout/custom-views/create-view#customattr
https://developer.android.com/develop/ui/views/layout/custom-views/create-view#applyattr

Related

Custom Button component: shadow is clipped

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" />

Material Cardview - rounded corner of TabLayout not getting clipped

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)
}
}
}

How to use CustomView in Android

In my application I create one custom view for show TextView and FontAwesome!
I write below codes, but not set values and just show default values!
TextWithIcon class :
class TextWithIcon #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyle, defStyleRes) {
init {
LayoutInflater.from(context).inflate(R.layout.layout_text_with_awesome, this, true)
orientation = VERTICAL
attrs?.let {
val typedArray = context.obtainStyledAttributes(R.styleable.TextWithIcon)
val title = resources.getText(
typedArray.getResourceId(
R.styleable.TextWithIcon_customText,
R.string.app_name
)
)
val icon = resources.getText(
typedArray.getResourceId(
R.styleable.TextWithIcon_customIcon,
R.string.app_name
)
)
val titleTxt = getChildAt(0) as TextView
titleTxt.text = title
Log.e("titleTxt",title.toString())
//binding.iconTxt.text = "&#x$icon"
typedArray.recycle()
}
}
}
CustomViewLayout file :
<?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">
<TextView
android:id="#+id/titleTxt"
android:layout_width="wrap_content"
android:layout_height="#dimen/_20mdp"
android:gravity="right"
android:textColor="#color/darkJungleGreen"
android:textSize="#dimen/_10font_mdp" />
<com.myapp.utils.views.FontAwesome
android:id="#+id/iconTxt"
android:layout_width="#dimen/_20mdp"
android:layout_height="#dimen/_20mdp"
android:layout_gravity="right"
android:gravity="center"
android:textColor="#color/beauBlue"
android:textSize="#dimen/_10font_mdp"
app:fontPath="fonts/fontawesome_re.ttf" />
</merge>
And I used this class with code in XML :
<com.myapp.TextWithIcon
android:id="#+id/item1Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="#dimen/_10mdp"
android:drawablePadding="#dimen/_5mdp"
android:gravity="center"
android:textColor="#color/ochre"
android:textSize="#dimen/_10font_mdp"
app:customIcon="f007"
app:customText="wegwergewrg"
app:fontPath="fonts/iransans_bold.ttf"
app:layout_constraintEnd_toStartOf="#id/item1Line"
app:layout_constraintTop_toTopOf="#id/item1Line" />
I set value with this code : app:customText="wegwergewrg", but not show this and just show default value from
resources.getText(
typedArray.getResourceId(
R.styleable.TextWithIcon_customText,
R.string.app_name
)
)
How can I fix it?
You are going to want your styleable to look something like this:
<resources>
<declare-styleable name="TextWithIcon">
<attr name="customText" format="reference|string" />
</declare-styleable>
</resources>
This definition will permit you to use text as well as a string resource id for "app:customText".
You can get the value from the attribute as follows:
class TestCustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.TextWithIcon)
for (attr in 0 until a.indexCount) {
when (a.getIndex(attr)) {
R.styleable.TextWithIcon_customText -> {
text = a.getText(attr) ?: "Unknown text."
}
}
}
a.recycle()
}
}

Custom TextView alignment

I wrote a small custom text view in order to use it on a list. There are three States depending on which i want to have this:
State 1 -> just the text, for example TEST_TEXT
State 2 -> a drawable start, a color background and the same text TEST_TEXT
State 3 -> a different drawable start, different color background and the same text TEST_TEXT
The text has always the same value (TEST_TEXT)
But with my implementation i lost the alignment and as a result the text on the state that
i do not have a drawable start is not align with the others that have drawable as you can see on the image below. I want all of them to be start align.
Is there a way to achieve it?
My custom text view class is :
enum class State {
STATE_ONE,
STATE_TWO,
STATE_THREE
}
class CustomTextView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): TextView(context, attrs, defStyleAttr) {
private val PADDING = 4 * resources.displayMetrics.density
private val ZERO_PADDING = 0 * resources.displayMetrics.density
var state: State = State.STATE_ONE
set(value) {
field = value
updateTextView()
invalidate()
}
private fun updateTextView() {
when(state) {
State.STATE_TWO -> {
this.setBackgroundResource(R.color.light_red)
this.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_baseline_message_24, 0, 0, 0)
this.compoundDrawablePadding = PADDING.toInt()
this.setPadding(PADDING.toInt(), PADDING.toInt(), PADDING.toInt(), PADDING.toInt())
}
State.STATE_THREE -> {
this.setBackgroundResource(R.color.colorGreenBright)
this.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_baseline_message_24, 0, 0, 0)
this.compoundDrawablePadding = PADDING.toInt()
this.setPadding(PADDING.toInt(), PADDING.toInt(), PADDING.toInt(), PADDING.toInt())
}
else -> {
this.setBackgroundResource(R.color.transparent)
this.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
this.compoundDrawablePadding = PADDING.toInt()
this.setPadding(ZERO_PADDING.toInt(), ZERO_PADDING.toInt(), ZERO_PADDING.toInt(), ZERO_PADDING.toInt())
}
}
}
}
The layout where i add them is this:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<custom.textView.CustomTextView
android:id="#+id/textViewOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TEST_TEXT"
android:textSize="12sp"
android:textColor="#color/black"
android:background="#drawable/drawable_rounded_text_bg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="30dp"/>
<custom.textView.CustomTextView
android:id="#+id/textViewTwo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TEST_TEXT"
android:textSize="12sp"
android:textColor="#color/black"
android:background="#drawable/drawable_rounded_text_bg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textViewOne"
android:layout_marginTop="10dp"/>
<custom.textView.CustomTextView
android:id="#+id/textViewThree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TEST_TEXT"
android:textSize="12sp"
android:textColor="#color/black"
android:background="#drawable/drawable_rounded_text_bg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textViewTwo"
android:layout_marginTop="10dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
and finally my activity is simple like this:
class CustomTextActivity: BaseActivity() {
private lateinit var binding: ActivityCustomTextBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(getView())
initLayout()
}
override fun getView(): View {
binding = ActivityCustomTextBinding.inflate(layoutInflater)
return binding.root
}
private fun initLayout() {
binding.textViewOne.state = State.STATE_ONE
binding.textViewTwo.state = State.STATE_TWO
binding.textViewThree.state = State.STATE_THREE
}
}
To my mind, if you want to have the same alignment, even without inner drawables, you have 2 options:
right align the text
display a "fake drawable" in the first case

How to set indicator in BottomNavigationView? [duplicate]

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>

Categories

Resources