I'm creating a custom compound view which extends LinearLayout (also tried ConstraintLayout) which contains 2 child views. When the app is running the view lays out correctly, however in the preview window it shows as 0 height when I use wrap_content. When the view was extending ConstraintLayout it was rendering as match_parent when the view was set to wrap_content.
If I override onMeasure and force a size when isInEditMode it is still showing up as 0 height.
Can someone point out what I'm doing wrong?
This is the custom view class:
package com.classdojo.android.nessie
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import com.airbnb.paris.utils.getFont
import com.classdojo.android.nessie.icon.Icon
import kotlinx.android.synthetic.main.nessie_button_view.view.*
class NessieButton : LinearLayout {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(attrs)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(attrs)
}
constructor(context: Context) : super(context) {
init(null)
}
enum class Style(
#DrawableRes
val backgroundRes: Int,
#ColorRes
val textColor: Int
) {
PRIMARY(
backgroundRes = R.drawable.nessie_button_style_primary,
textColor = R.color.nessie_button_style_primary_text_color
),
SECONDARY(
backgroundRes = R.drawable.nessie_button_style_secondary,
textColor = R.color.nessie_button_style_secondary_text_color
),
TERTIARY(
backgroundRes = R.drawable.nessie_button_style_tertiary,
textColor = R.color.nessie_button_style_tertiary_text_color
),
DESTRUCTIVE(
backgroundRes = R.drawable.nessie_button_style_destructive,
textColor = R.color.nessie_button_style_destructive_text_color
),
BEYOND(
backgroundRes = R.drawable.nessie_button_style_beyond,
textColor = R.color.nessie_button_style_beyond_text_color
);
companion object {
fun from(index: Int) = values().getOrElse(index) { Style.PRIMARY }
}
}
enum class Size(
#DimenRes
val horizontalPadding: Int,
#DimenRes
val verticalPadding: Int,
#DimenRes
val textSize: Int
) {
SMALL(
horizontalPadding = R.dimen.nessie_default_size_3x,
verticalPadding = R.dimen.nessie_default_size_2x,
textSize = R.dimen.nessie_textSizeDetail
),
MEDIUM(
horizontalPadding = R.dimen.nessie_default_size_3x,
verticalPadding = R.dimen.nessie_default_size_2x,
textSize = R.dimen.nessie_textSizeAction
),
LARGE(
horizontalPadding = R.dimen.nessie_default_size_4x,
verticalPadding = R.dimen.nessie_default_size_3x,
textSize = R.dimen.nessie_textSizeAction
);
companion object {
fun from(index: Int) = values().getOrElse(index) { Size.LARGE }
}
}
var text: CharSequence?
set(value) {
nessie_button_text_view.text = value
nessie_button_text_view.isVisible = !value.isNullOrBlank()
}
get() = nessie_button_text_view.text
var icon: Icon? = null
set(value) {
field = value
when (value) {
null -> {
nessie_button_icon_view.isVisible = false
}
else -> {
nessie_button_icon_view.icon = value
nessie_button_icon_view.isVisible = true
}
}
}
var style: Style = Style.PRIMARY
set(value) {
field = value
setBackgroundResource(value.backgroundRes)
val color = ResourcesCompat.getColor(resources, value.textColor, null)
nessie_button_icon_view.iconColor = color
nessie_button_text_view.setTextColor(color)
}
var size: Size = Size.LARGE
set(value) {
field = value
val horizontalPadding = resources.getDimensionPixelSize(value.horizontalPadding)
val verticalPadding = resources.getDimensionPixelSize(value.verticalPadding)
setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
val fontSize = resources.getDimension(value.textSize)
nessie_button_text_view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
nessie_button_icon_view.apply {
iconPixelSize = fontSize.toInt()
layoutParams.apply {
width = fontSize.toInt()
height = fontSize.toInt()
}
}
}
init {
init(null)
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
super.setFocusable(enabled)
val colorStateList = ResourcesCompat.getColorStateList(resources, style.textColor, null)
val viewState = drawableState
val color = colorStateList!!.getColorForState(viewState, 0)
nessie_button_text_view.setTextColor(color)
nessie_button_icon_view.iconColor = color
}
private fun init(attrs: AttributeSet?) {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
inflate(context, R.layout.nessie_button_view, this)
val styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.nessie_NessieButton)
try {
val iconIndex = styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_icon, -1)
icon = when (iconIndex) {
-1 -> null
else -> Icon.from(iconIndex)
}
text = styledAttributes.getString(R.styleable.nessie_NessieButton_android_text)
style = Style.from(styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_buttonStyle, Style.PRIMARY.ordinal))
size = Size.from(styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_buttonSize, Size.LARGE.ordinal))
isEnabled = styledAttributes.getBoolean(R.styleable.nessie_NessieButton_android_enabled, true)
} finally {
styledAttributes.recycle()
}
nessie_button_text_view.typeface = context.getFont(R.font.nessie_proximanova_bold)
}
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
if (child.id == R.id.nessie_button_icon_view || child.id == R.id.nessie_button_text_view) {
super.addView(child, index, params)
} else {
throw IllegalArgumentException("Cannot add more views to a NessieButton")
}
}
}
And this is the view layout:
<?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:id="#+id/root_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="#dimen/nessie_default_size_2x"
tools:background="#drawable/nessie_button_primary_background"
tools:parentTag="android.widget.LinearLayout">
<com.classdojo.android.nessie.icon.IconImageView
android:id="#+id/nessie_button_icon_view"
android:layout_width="#dimen/nessie_default_size_3x"
android:layout_height="#dimen/nessie_default_size_3x"
android:layout_marginEnd="#dimen/nessie_default_size" />
<TextView
android:id="#+id/nessie_button_text_view"
style="#style/nessie_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineHeight="22sp"
android:textStyle="bold"
tools:text="My button"
tools:textColor="#color/nessie_white" />
</merge>
It turns out that it wasn't related to the merge/custom view, but was related to the fact that the editor preview couldn't find the font resource.
Wrapping the instances where the font was loaded in if (!isInEditMode){} resolved the problem.
I knew there was an errors list, but I hadn't used it in a while and couldn't find it. In case someone comes across this question, it's the blue i icon in the top right which shows you the errors hit when rendering the view.
<merge is not a ViewGroup. Hence, the android:attributes on that tag are ignored. This means, that your view does not have an id, width, height, gravity nor does it have padding.
Either add the attributes to where you are using your custom view like so:
<com.classdojo.android.nessie.NessieButton
android:id="#+id/root_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="#dimen/item_spacing_normal"/>
Or, if these attributes should be the same for all NessieButton's, set these properties programatically. Inside your init() methods sounds like the best place.
Be careful with the android:id as well! You probably do not want to set it programatically. But you will almost certainly want to set it, when you actually use your NessieButton.
Related
I want to add a indeterminate progressbar to my custom button.
I'm using add setCompoundDrawablesWithIntrinsicBounds to add it to left.
But somehow progress bar animation doesn't appear.
//CustomButton.kt
fun addProgressbar () {
val progressBar = ProgressBar(context)
progressBar.isIndeterminate = true
progressBar.animate()
progressBar.setBackgroundColor(Color.RED)
progressBar.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
progressBar.visibility = View.VISIBLE
setCompoundDrawablesWithIntrinsicBounds(
progressBar.indeterminateDrawable,
null,
ContextCompat.getDrawable(context, R.drawable.ic_baseline_notifications_black_24),
null
)
}
When I debug progressBar.isAnimating() returns false. I also tried to add a resource drawable with ContextCompat.getDrawable which works.
How can I make it work ?
class MyButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AppCompatButton(context, attrs, defStyle) {
private var progressDrawable: Drawable
init {
progressDrawable = ProgressBar(context).indeterminateDrawable.apply {
// apply any customization on drawable. not on progress view
setBounds(0, 0, 24.toPx, 24.toPx)
setTint(Color.WHITE)
}
compoundDrawablePadding = 4.toPx
}
var isLoading: Boolean = false
set(value) {
if (isLoading == value) return
field = value
val (startDrawable, topDrawable, endDrawable, bottomDrawable) = compoundDrawablesRelative
if (value) {
// add progress and keep others
setCompoundDrawablesRelative(
progressDrawable,
topDrawable,
endDrawable,
bottomDrawable
)
(progressDrawable as? Animatable)?.start()
} else {
// remove progress
setCompoundDrawablesRelative(
null,
topDrawable,
endDrawable,
bottomDrawable
)
(progressDrawable as? Animatable)?.stop()
}
}
override fun onDetachedFromWindow() {
(progressDrawable as? Animatable)?.stop()
super.onDetachedFromWindow()
}
}
PS: Slowness is related to recording/encoding. It works normally.
I am building a stock ticker type carousel that has market prices running along the screen as shown. The 'CRC' Ticker has only the partial view showing, it clips off at the edge of the parent which is the width of the device even when it is animated into view. I want it to have a width large enough hold all the children that goes past the device width.
Here if the Carousel layout xml:
<?xml version="1.0" encoding="utf-8"?>
<com.android.forexwatch.views.timepanel.CarouselView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/carouselLinear"
android:layout_width="match_parent"
android:layout_height="30dp"
android:orientation="horizontal"
android:singleLine="true"
android:animateLayoutChanges="true"
/>
Here is the class:
package com.android.forexwatch.views.timepanel
import android.content.Context
import android.util.AttributeSet
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.appcompat.widget.LinearLayoutCompat
import com.android.forexwatch.model.Index
import com.android.forexwatch.R
import com.android.forexwatch.model.CityInfoDetailed
import com.github.ybq.android.spinkit.SpinKitView
class CarouselView(context: Context, attrs: AttributeSet): LinearLayoutCompat(context, attrs) {
companion object {
private const val IndexWidth = 600F
}
private var contextThemeWrapper: ContextThemeWrapper? = null
init {
contextThemeWrapper = ContextThemeWrapper(context.applicationContext, R.style.Theme_ForexWatch)
}
fun setupView ( cityInfo: CityInfoDetailed?, isMirror: Boolean = false) {
createPriceTickerViews(cityInfo, isMirror)
}
private fun createPriceTickerViews(cityInfo: CityInfoDetailed?, isMirror: Boolean = false) {
destroy()
removeAllViews()
var calculatedWidth = 0
cityInfo?.indexes?.forEachIndexed {
index, element ->
if (isMirror) index.plus(cityInfo?.indexes.count())
val loader: SpinKitView = LayoutInflater.from(contextThemeWrapper).inflate(R.layout.preloader, null) as SpinKitView
val priceTicker: PriceTicker = LayoutInflater.from(contextThemeWrapper).inflate(R.layout.price_ticker, null) as PriceTicker
addView(priceTicker)
addView(loader)
// priceTicker.x = IndexWidth * index
calculatedWidth.plus(IndexWidth)
priceTicker.initialize(element, cityInfo, loader)
}
layoutParams.width = calculatedWidth
}
private fun destroy() {
for (idx in 0 .. this.childCount) {
val view = getChildAt(idx)
if (view is PriceTicker) {
view.destroy()
}
}
}
}
Here is the parent class:
package com.android.forexwatch.views.timepanel
import android.animation.ValueAnimator
import android.graphics.Color
import android.view.View
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.android.forexwatch.R
import com.android.forexwatch.adapters.TimePanelRecyclerViewAdapter
import com.android.forexwatch.events.TimeEvent
import com.android.forexwatch.model.CityInfoDetailed
import com.android.forexwatch.model.TimeObject
import com.android.forexwatch.utils.TimeKeeper
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
class TimePanelView(view: View, container: ViewGroup, timePanelAdapter: TimePanelRecyclerViewAdapter) : RecyclerView.ViewHolder(view) {
companion object {
private const val OffsetTime: Long = 15 * 60
private const val FrameRate: Long = 32
}
private var fxTime: TextView? = null
private var stockTime: TextView? = null
private var city: TextView? = null
private var currentTime: TextView? = null
private var fxTimeLabel: TextView? = null
private var stockTimeLabel: TextView? = null
private var carouselView: CarouselView? = null
private var carouselViewMirror: CarouselView? = null
private var parentContainer: ViewGroup? = null
private var adapter: TimePanelRecyclerViewAdapter? = null
private var cityInfo: CityInfoDetailed? = null
private var view: View? = null
init {
adapter = timePanelAdapter
this.view = view
this.parentContainer = container
EventBus.getDefault().register(this)
}
fun setupView(cityInfo: CityInfoDetailed?) {
this.cityInfo = cityInfo
city = view?.findViewById(R.id.city)
stockTime = view?.findViewById(R.id.stockTime)
fxTime = view?.findViewById(R.id.fxTime)
fxTimeLabel = view?.findViewById(R.id.fxTimeLabel)
stockTimeLabel = view?.findViewById(R.id.stockTimeLabel)
currentTime = view?.findViewById(R.id.currentTime)
carouselView = view?.findViewById(R.id.carouselLinear)
carouselViewMirror = view?.findViewById(R.id.carouselLinearMirror)
city?.text = cityInfo?.cityName
createCarousel()
}
#Subscribe
fun update(event: TimeEvent.Interval?) {
updateCurrentTime()
updateFxTime()
updateStockTime()
}
private fun createCarousel() {
carouselView?.setupView(cityInfo)
carouselViewMirror?.setupView(cityInfo, true)
carouselView?.bringToFront()
carouselViewMirror?.bringToFront()
animateCarousel()
}
private fun updateCurrentTime() {
val timezone: String? = cityInfo?.timeZone
currentTime?.text = TimeKeeper.getCurrentTimeWithTimezone(timezone, "EEE' 'HH:mm:ss")
}
private fun updateFxTime() {
updateTime(fxTime, fxTimeLabel, cityInfo?.forexOpenTimes, cityInfo?.forexCloseTimes, cityInfo?.timeZone, "FX", Companion.OffsetTime * 8)
}
private fun updateStockTime() {
updateTime(stockTime, stockTimeLabel, cityInfo?.stockOpenTime, cityInfo?.stockCloseTime, cityInfo?.timeZone, cityInfo?.stockExchangeName, Companion.OffsetTime)
}
private fun updateTime(timeView: TextView?, timeLabel: TextView?, open: TimeObject?, close: TimeObject?, timezone: String?, type: String?, timeOffset: Long) {
if (TimeKeeper.isMarketOpen(open, close, timezone)) {
timeView?.text = TimeKeeper.getTimeDifference(close, close, timezone, true)
displayMarketColors(timeView, timeOffset, close, timezone, Color.GREEN, true)
timeLabel?.text = "To " + type + " Close"
} else {
timeView?.text = TimeKeeper.getTimeDifference(open, close, timezone, false)
displayMarketColors(timeView, timeOffset, open, timezone, Color.RED, false)
timeLabel?.text = "To " + type + " Open"
}
}
private fun displayMarketColors(timeView: TextView?, timeOffset: Long, time: TimeObject?, timezone: String?, outRangeColor: Int, isMarketOpen: Boolean?) {
val color = if (TimeKeeper.isTimeWithinRange(timeOffset, time, timezone, isMarketOpen)) Color.parseColor("#FF7F00") else outRangeColor
timeView?.setTextColor(color)
}
private fun animateCarousel() {
if (cityInfo?.indexes?.count() == 1) {
return
}
// carouselView?.x = carouselView?.x?.minus(3.0F)!!
/* CoroutineScope(Dispatchers.Main).launch {
delay(FrameRate)
animateCarousel()
}*/
val animator = ValueAnimator.ofFloat(0.0f, 1.0f)
animator.repeatCount = ValueAnimator.INFINITE
animator.interpolator = LinearInterpolator()
animator.duration = 18000L
animator.addUpdateListener { animation ->
val progress = animation.animatedValue as Float
val width: Int? = carouselView?.width
val translationX = -width?.times(progress)!!
carouselView?.translationX = translationX!!
carouselViewMirror?.translationX = translationX!! + width!!
}
animator.start()
}
}
The PriceTicker layout which extends AppCompatTextView
<com.android.forexwatch.views.timepanel.PriceTicker
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
class="com.android.forexwatch.views.timepanel.PriceTicker"
android:layout_width="240dp"
android:layout_height="match_parent"
android:textAlignment="center"
android:textSize="13sp"
android:singleLine="true"
android:background="#drawable/rectangle_shape"
app:layout_constraintBottom_toBottomOf="parent"
/>
I found the solution by invalidating and requestingLayout on the priceTicker TextView and the container. Also changed the width of the container and priceTicker to the desired width.
calculatedWidth = calculatedWidth.plus(IndexWidth).toInt()
priceTicker.initialize(index, cityInfo, loader)
priceTicker.width = IndexWidth.toInt()
resetLayout(priceTicker)
}
layoutParams.width = (calculatedWidth)
resetLayout(thisContainer)
}
}
private fun destroy() {
for (idx in 0 .. this.childCount) {
val view = getChildAt(idx)
if (view is PriceTicker) {
view.destroy()
}
}
}
private fun resetLayout(view: View?) {
view?.invalidate()
view?.requestLayout()
}
I'm updating the font size of all the texts on the app, what I want to achieve is, when I select the font size, i should be able to update the font sizes of all the texts on that activity.
My only problem is i can't find the size property on the Spinner Object.
This is what I did for Text Views, is it possible to apply a code similar to this one for Spinners ?
const val HEADER_TEXT = 24
const val NORMAL_TEXT = 14
private fun updateAssetSize(textView: TextView, additionalSize: Int, type: Int) {
val size = additionalSize + type
textView.setTextSize(COMPLEX_UNIT_SP, size.toFloat());
}
//calling the method:
updateAssetSize(screenText, additionalFontSize, HEADER_TEXT)
Note: This should be done from code, since this will be updated on run time.
Based on #Zain Suggestion, I resolved this by using an adapterlist object. Instead of using String I created a custom class with fontSize and text properties in it.
class SpinnerItem(
val text: String,
var fontSize: Int
) {
// this is necessary, in order for the text to display the texts in the dropdown list
override fun toString(): String {
return text
}
}
Here's the AdapterList that I created:
class SpinnerItemListAdapter(
context: Context,
val resourceId: Int,
var list: ArrayList<SpinnerItem>
) : ArrayAdapter<SpinnerItem>(context, resourceId, list) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val text = this.list[position].text
val size = this.list[position].fontSize
val inflater = LayoutInflater.from(context)
val convertView = inflater.inflate(resourceId, parent, false)
val simpleTextView = convertView.findViewById(R.id.simpleTextView) as TextView
simpleTextView.text = text
simpleTextView.setTextSize(size.toFloat())
return convertView
}
// We'll call this whenever there's an update in the fontSize
fun swapList(list: ArrayList<SpinnerItem>) {
clear()
addAll(list)
notifyDataSetChanged()
}
}
Here's the custom XML File spinner_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/simpleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:padding="12dp"
android:textSize="16sp" />
The Spinner to be updated:
var fontSizes = arrayListOf(
SpinnerItem("Small", NORMAL_TEXT, "Default"),
SpinnerItem("Normal", NORMAL_TEXT, "Default"),
SpinnerItem("Large", NORMAL_TEXT, "Default"),
SpinnerItem("Largest", NORMAL_TEXT, "Default")
)
var fontSizeAdapterItem = SpinnerItemListAdapter(
this,
R.layout.spinner_item,
toSpinnerItemList(fontSizes, newSize)
)
Here's What will happen when we update it:
private fun updateSpinnerSize(additional: Int) {
val newSize = additional + NORMAL_TEXT
fontSizes = toSpinnerItemList(fontSizes, newSize)
fontSizeAdapterItem?.let {
it.swapList(fontSizes)
}
}
private fun toSpinnerItemList(
list: ArrayList<SpinnerItem>,
newSize: Int
): ArrayList<SpinnerItem> {
val itemList = ArrayList<SpinnerItem>()
for (item in list) {
item.fontSize = newSize
itemList.add(item)
}
return itemList
}
I'm working on Android TextView animation.
Requirement is TextView is at fix location of the screen and every character should be animate with alpha (Lower to higher). I've tried couple of libraries unfortunately it doesn’t work for me.
Reference screenshot:
If anybody has solution for this, kindly provide it. Thanks
After doing lots of research on the same, I'm posting my own answer.
Steps:
Create CustomTextLayout
class CustomTextLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayoutCompat(context, attrs, defStyleAttr) {
private var characterAnimationTime = 100
private var textSize = 22f
private var letterSpacing = 0f
private var animationDuration = 2000L
init {
orientation = HORIZONTAL
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomTextLayout, defStyleAttr, 0)
textSize = typedArray.getFloat(R.styleable.CustomTextLayout_textSize, textSize)
typedArray.recycle()
}
/**
* This function sets the animated alpha text
* #param context Context of Activity / Fragment
* #param text Text string
* #param initialDelay Start animation delay
*/
fun setAnimatedText(context: Context, text: String, initialDelay: Long = 0) {
var textDrawPosition = 0
Handler().postDelayed({
for (char in text) {
val textView = getTextView(char.toString())
textView.visibility = View.GONE
this.addView(textView)
textDrawPosition++
drawAnimatedText(
context,
this,
textView,
textDrawPosition,
text,
(textDrawPosition * characterAnimationTime).toLong()
)
}
}, initialDelay)
}
private fun drawAnimatedText(
context: Context,
parentView: LinearLayoutCompat,
textView: AppCompatTextView,
position: Int,
text: String,
initialDelay: Long
) {
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), Color.WHITE, Color.BLACK)
colorAnimation.startDelay = initialDelay
colorAnimation.duration = animationDuration
colorAnimation.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animator: Animator) {
textView.visibility = View.VISIBLE
}
override fun onAnimationEnd(animator: Animator) {
if (position == text.length) {
val updatedTextView = getTextView(text)
updatedTextView.setTextColor(Color.BLACK)
updatedTextView.visibility = View.VISIBLE
parentView.removeAllViews()
parentView.addView(updatedTextView)
}
}
override fun onAnimationCancel(animator: Animator) {
}
override fun onAnimationRepeat(animator: Animator) {
}
})
colorAnimation.addUpdateListener {
textView.setTextColor(it.animatedValue as Int)
}
colorAnimation.start()
}
private fun getTextView(text: String): AppCompatTextView {
val textView = AppCompatTextView(context)
textView.text = text
textView.textSize = textSize
textView.setTypeface(Typeface.SANS_SERIF, Typeface.ITALIC)
textView.letterSpacing = letterSpacing
return textView
}
Add in layout file
<com.mypackagename.CustomTextLayout
app:textSize="30"
app:letterSpacing="0.1"
android:id="#+id/textLayoutFirst"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
</com.mypackagename.CustomTextLayout>
Add attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomTextLayout">
<attr name="textSize" format="float"/>
<attr name="letterSpacing" format="float"/>
</declare-styleable>
</resources>
Start animation:
textLayoutFirst.setAnimatedText(this, "Some text here")
It's done.
Currently it looks as in attached with this layout:
<android.support.design.widget.TextInputLayout
android:id="#+id/layoutCurrentPW"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:errorEnabled="true">
How to set the error message "password must at least be 8 characters" to center gravity ?
I tried with android:gravity="center" but that did not work.
EDIT
Layout that includes EditText:
<android.support.design.widget.TextInputLayout
android:id="#+id/layoutCurrentPW"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:errorEnabled="true">
<EditText
android:id="#+id/editTextCurrentPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:gravity="center"
android:hint="#string/current_password"
android:inputType="textPassword"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#color/black" />
</android.support.design.widget.TextInputLayout>
I wanted to know if there is any way to handle it from framework..seems no.
But the way TextInputLayout work is:
- hint will be shown on top of EditText when user touches it.
- Error messages will be shown just under the TextInputLayout and aligned to start.
I had 40dp of left_margin to my EditText due to which misalignment between hint and error message. So for now, I removed left_margin 40dp from EditText and applied same to TextInputLayout itself so it looks fine now.
Lesson learnt :-) is if any margins has to be applied to EditText, better same, if possible, can be applied to TextInputLayout to keep hint and error messages to be placed properly.
class CenterErrorTextInputLayout(context: Context, attrs: AttributeSet) : TextInputLayout(context, attrs) {
override fun setErrorTextAppearance(resId: Int) {
super.setErrorTextAppearance(resId)
val errorTextView = this.findViewById<TextView>(R.id.textinput_error)
val errorFrameLayout = errorTextView.parent as FrameLayout
errorTextView.gravity = Gravity.CENTER
errorFrameLayout.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
}}
Here a solution that always centers the errormessage no matter how big your TextInputLayout is.
You make a own class that inherits from TextInputLayout. Then override the ShowError(string text, Drawable icon) method. If the error is called you center the textView with the error.
public class TextInputLayout_Center : TextInputLayout
{
public override void ShowError(string text, Android.Graphics.Drawables.Drawable icon)
{
base.ShowError(text, icon);
centerErrorMessage(this);
}
void centerErrorMessage(ViewGroup view)
{
for (int i = 0; i < view.ChildCount; i++)
{
View v = view.GetChildAt(i);
if (v.GetType() == typeof(TextView))
{
v.LayoutParameters = new LayoutParams(ViewGroup.LayoutParams.MatchParent, v.LayoutParameters.Height);
((TextView)v).Gravity = GravityFlags.CenterHorizontal;
((TextView)v).TextAlignment = TextAlignment.Center;
}
if (v is ViewGroup)
{
centerErrorMessage((ViewGroup)v);
}
}
}
}
I achieved it by setting start margin for the error view. Below you can see my overriden version of setError method of the TextInputLayout component.
#Override
public void setError(#Nullable CharSequence errorText) {
// allow android component to create error view
if (errorText == null) {
return;
}
super.setError(errorText);
// find this error view and calculate start margin to make it look like centered
final TextView errorTextInput = (TextView) findViewById(R.id.textinput_error);
errorTextInput.measure(0, 0);
int errorWidth = errorTextInput.getMeasuredWidth();
int layoutWidth = getWidth();
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) errorTextInput.getLayoutParams();
params.setMarginStart((layoutWidth - errorWidth) / 2);
errorTextInput.setLayoutParams(params);
}
#Override
public void setErrorEnabled(boolean enabled) {
super.setErrorEnabled(enabled);
if (!enabled)
return;
try {
setErrorGravity(this);
} catch (Exception e) {
e.printStackTrace();
}
}
private void setErrorGravity(ViewGroup view) throws Exception {
for (int i = 0; i < view.getChildCount(); i++) {
View errorView = view.getChildAt(i);
if (errorView instanceof TextView) {
if (errorView.getId() == com.google.android.material.R.id.textinput_error) {
FrameLayout errorViewParent = (FrameLayout) errorView.getParent();
errorViewParent.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
((TextView) errorView).setGravity(Gravity.RIGHT);
((TextView) errorView).setTypeface(FontUtils.getTypeFace(view.getContext(), FontUtils.FONT_NAZANIN_TAR));
}
}
if (errorView instanceof ViewGroup) {
setErrorGravity((ViewGroup) errorView);
}
}
}
Hi if you using last version of material design (v 1.2 and above), you have to use this way: (I set gravity to end for support rtl)
Thanks #Emmanuel Guerra
class MyTextInputLayout : TextInputLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttrs: Int) : super(context, attrs, defStyleAttrs)
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) {
return
}
try {
changeTextAlignment(com.google.android.material.R.id.textinput_error, View.TEXT_ALIGNMENT_VIEW_END)
val errorView: ViewGroup = this.getChildAt(1) as LinearLayout
val params: LinearLayout.LayoutParams = errorView.layoutParams as LinearLayout.LayoutParams
params.gravity = Gravity.END
errorView.layoutParams = params
//errorView.setPadding(0, 0, 0, 0) //use this to remove error text padding
//setErrorIconDrawable(0)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun changeTextAlignment(textViewId: Int, alignment: Int) {
val textView = findViewById<TextView>(textViewId)
textView.textAlignment = alignment
}
}
A custom TextInputLayout class for for aligning the error text.
class CenterErrorTextInputLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : TextInputLayout(context, attrs, defStyleAttr) {
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) return
try {
setErrorTextAlignment()
} catch (e: Exception) {
Timber.e(e, "Failed to set error text : RightErrorTextInputLayout")
}
}
private fun setErrorTextAlignment() {
val errorView: TextView = this.findViewById(R.id.textinput_error)
errorView.textAlignment = View.TEXT_ALIGNMENT_CENTER
}
}
To align the text to the end, use View.TEXT_ALIGNMENT_VIEW_END instead.