I've an EditText enclosed within a TextInputLayout. I wish to display errors under the EditText, but aligned to the right end of the screen.
This is what I currently have:
The error is displayed like this:
What I want it to look like:
My XML is this:
<android.support.design.widget.TextInputLayout
android:id="#+id/text_input_email"
style="#style/forgot_pass_text_inputlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/tv_enter_email_message"
android:layout_marginTop="#dimen/email_padding_top"
android:hintTextAppearance="#{forgotPasswordVM.hintTextAppearance}"
android:theme="#style/forgot_pass_til_state"
app:error="#{forgotPasswordVM.email.textInputError}">
<EditText
android:id="#+id/actv_email"
style="#style/forgot_pass_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email_address"
android:imeActionId="#+id/btn_submit"
android:imeOptions="actionSend"
android:inputType="textEmailAddress"
android:maxLines="1"
android:onEditorAction="#{forgotPasswordVM.onEditorAction}"
android:singleLine="true"
app:binding="#{forgotPasswordVM.email.text}" />
</android.support.design.widget.TextInputLayout>
I'm using data-binding.
I've tried setting android:gravity="right" and android:layout_gravity="right" on both the EditText and the TextInputLayout. Neither works the way I want it to.
I've tried setting right gravity in the theme as well as the style. Neither has any effect on the error.
I've tried right gravity on the style that is applied within app:errorTextAppearance="#style/right_error". Even this doesn't work.
I tried programmatically shifting the Error to the right by using a SpannableStringBuilder with an AlignmentSpan following this link. Even that doesn't work for me.
I'm not sure what else to try. Please suggest.
So thanks to Mike's answer I was able to figure out the solution.
I created a custom class:
public class CustomTextInputLayout extends TextInputLayout {
public CustomTextInputLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public void setErrorEnabled(boolean enabled) {
super.setErrorEnabled(enabled);
if (!enabled) {
return;
}
try {
Field errorViewField = TextInputLayout.class.getDeclaredField("mErrorView");
errorViewField.setAccessible(true);
TextView errorView = (TextView) errorViewField.get(this);
if (errorView != null) {
errorView.setGravity(Gravity.RIGHT);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.END;
errorView.setLayoutParams(params);
}
}
catch (Exception e) {
// At least log what went wrong
e.printStackTrace();
}
}
}
Now I simply replaced my TextInputLayout with the CustomTextInputLayout.
Works like a charm. Thanks a lot Mike. I wish I could credit you more for this answer.
Here's a hack in Kotlin that doesn't depend on view hierarchy:
class CustomTextInputLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextInputLayout(context, attrs, defStyleAttr) {
override fun setError(errorText: CharSequence?) {
super.setError(errorText)
with(findViewById<TextView>(R.id.textinput_error)) {
// this will work as long as errorView's layout width is
// MATCH_PARENT -- it is, at least now in material:1.2.0-alpha06
textAlignment = TextView.TEXT_ALIGNMENT_VIEW_END
}
}
}
My custom class is in Kotlin
class CustomTextInputLayout(context: Context, attrs: AttributeSet) :
TextInputLayout(context, attrs) {
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) {
return
}
try {
val layout = this
val errorView : TextView = ((this.getChildAt(1) as ViewGroup).getChildAt(0) as ViewGroup).getChildAt(0) as TextView
(layout.getChildAt(1) as ViewGroup).layoutParams.width = LayoutParams.MATCH_PARENT
(layout.getChildAt(1) as ViewGroup).getChildAt(0).layoutParams.width = FrameLayout.LayoutParams.MATCH_PARENT
errorView.gravity = Gravity.END
} catch (e: Exception) {
e.printStackTrace()
}
}
}
With material-componets version 1.2.0-rc01, you can create a CustomTextInputLayout that inherits from TextInputLayout to change the text alignment to the end (or where you want it to be) like this:
class CustomTextInputLayout : 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
)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun changeTextAlignment(textViewId: Int, alignment: Int) {
val textView = findViewById<TextView>(textViewId)
textView.textAlignment = alignment
}
}
The alignment is specified in the IndicatorViewController class to always be View.TEXT_ALIGNMENT_VIEW_START. This class is used by the TextInputLayout.
From the IndicatorViewController class
void setErrorEnabled(boolean enabled) {
if (errorEnabled == enabled) {
return;
}
cancelCaptionAnimator();
if (enabled) {
errorView = new AppCompatTextView(context);
errorView.setId(R.id.textinput_error);
if (VERSION.SDK_INT >= 17) {
errorView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
}
//More code...
If selected answer not works. Another solution is below:
Create custom class as below:
public class CustomTextInputLayout extends TextInputLayout
{
public CustomTextInputLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public void setErrorEnabled(boolean enabled)
{
super.setErrorEnabled(enabled);
if (!enabled)
{
return;
}
try
{
TextView errorView = this.findViewById(R.id.textinput_error);
FrameLayout errorViewParent = (FrameLayout) errorView.getParent();
errorViewParent.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.WRAP_CONTENT));
errorView.setGravity(Gravity.CENTER); // replace CENTER with END or RIGHT
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
Then create your custom TextInputLayout as:
<CustomTextInputLayout
android:id="#+id/til_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/activity_horizontal_margin"
app:layout_constraintBottom_toTopOf="#+id/til_last_name">
<EditText
android:id="#+id/et_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/first_name"
android:gravity="right"
/>
</CustomTextInputLayout>
After that initialize in your code as:
private CustomTextInputLayout tilFirstName;
tilFirstName = view.findViewById(R.id.til_first_name);
Congratulations! You have done what you have required.
Based on #Emmanuel Guerra's answer. I've applied it on helperText too:
class CustomTextInputLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.textInputStyle
) : TextInputLayout(context, attrs, defStyleAttr) {
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) return
centerMiniText(com.google.android.material.R.id.textinput_error)
}
override fun setHelperTextEnabled(enabled: Boolean) {
super.setHelperTextEnabled(enabled)
if (!enabled) return
centerMiniText(com.google.android.material.R.id.textinput_helper_text)
}
private fun centerMiniText(textViewId: Int) {
try {
changeTextAlignment(textViewId, View.TEXT_ALIGNMENT_CENTER)
} catch (e: Exception) {
e.printStackTrace()
}
}
#Suppress("SameParameterValue")
private fun changeTextAlignment(textViewId: Int, alignment: Int) {
findViewById<TextView>(textViewId)?.textAlignment = alignment
}
}
Related
I've created a custom view for selecting days of the week which results is a string. I'd like to use it with two-way data binding.
<?xml version="1.0" encoding="utf-8"?><com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/daypicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ToggleButton
android:id="#+id/tMon"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Mon"
android:textOn="#string/Mon" />
<ToggleButton
android:id="#+id/tTue"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Tue"
android:textOn="#string/Tue" />
<ToggleButton
android:id="#+id/tWed"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Wed"
android:textOn="#string/Wed" />
<ToggleButton
android:id="#+id/tThu"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Thu"
android:textOn="#string/Thu" />
<ToggleButton
android:id="#+id/tFri"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Fri"
android:textOn="#string/Fri" />
<ToggleButton
android:id="#+id/tSat"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Sat"
android:textOn="#string/Sat" />
<ToggleButton
android:id="#+id/tSun"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Sun"
android:textOn="#string/Sun" />
</LinearLayout></com.google.android.material.textfield.TextInputLayout>
And class to service:
class DayPicker : TextInputLayout {
var days: MutableSet<DayOfWeek> = HashSet()
private lateinit var tMon: ToggleButton
private lateinit var tTue: ToggleButton
private lateinit var tWed: ToggleButton
private lateinit var tThu: ToggleButton
private lateinit var tFri: ToggleButton
private lateinit var tSat: ToggleButton
private lateinit var tSun: ToggleButton
var mContext: Context? = null
constructor(context: Context) : super(context) {
mContext = context
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
mContext = context
initControl(context)
initDays()
initListeners()
}
private fun initListeners() {
initListener(tMon, DayOfWeek.MONDAY)
initListener(tTue, DayOfWeek.TUESDAY)
initListener(tWed, DayOfWeek.WEDNESDAY)
initListener(tThu, DayOfWeek.THURSDAY)
initListener(tFri, DayOfWeek.FRIDAY)
initListener(tSat, DayOfWeek.SATURDAY)
initListener(tSun, DayOfWeek.SUNDAY)
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
) {
mContext = context
}
#BindingAdapter("selectedDays")
fun setSelectedDays(dayPicker: DayPicker, selectedDays: String?) {
days = (selectedDays?.split(",")?.map { id -> DayOfWeek.of(Integer.parseInt(id)) }?.toSet()
?: HashSet()) as MutableSet<DayOfWeek>
}
#InverseBindingAdapter(attribute = "selectedDays")
fun getSelectedDays(dayPicker: DayPicker): String {
if (days.isEmpty()) {
this.error = "emptyy"
}
return days.map { x -> x.value }.joinToString(",")
}
#BindingAdapter("selectedDaysAttrChanged")
fun setSelectedDaysChangedListener(dayPicker: DayPicker, listener: InverseBindingListener) {
listener.onChange()
}
/**
* Load component XML layout
*/
private fun initControl(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.daypicker, this, true)
// layout is inflated, assign local variables to components
tMon = findViewById(R.id.tMon)!!
tTue = findViewById(R.id.tTue)!!
tWed = findViewById(R.id.tWed)!!
tThu = findViewById(R.id.tThu)!!
tFri = findViewById(R.id.tFri)!!
tSat = findViewById(R.id.tSat)!!
tSun = findViewById(R.id.tSun)!!
}
fun initDays() {
this.days.forEach { day ->
if (day == DayOfWeek.MONDAY) {
tMon.isChecked = true
} else if (day == DayOfWeek.TUESDAY) {
tTue.isChecked = true
} else if (day == DayOfWeek.WEDNESDAY) {
tWed.isChecked = true
} else if (day == DayOfWeek.THURSDAY) {
tThu.isChecked = true
} else if (day == DayOfWeek.FRIDAY) {
tFri.isChecked = true
} else if (day == DayOfWeek.SATURDAY) {
tSat.isChecked = true
} else if (day == DayOfWeek.SUNDAY) {
tSun.isChecked = true
}
}
}
fun initListener(button: ToggleButton, day: DayOfWeek) {
button.setOnClickListener {
if (button.isChecked) {
days.add(day)
} else {
days.remove(day)
}
}
}
}
When I use it in activity/fragment:
<.....textview.DayPicker
android:id="#+id/daypicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:visibleOrGone="#{viewModel.isSelectedDays().ld}"
app:selectedDays="#={viewModel.treatment.selected_days}"/>
I receive an error:
.../androidApp/build/generated/source/kapt/debug/com/package/android/databinding/FragmentAddStep2BindingImpl.java:41:
error: expected
java.lang.String callbackArg_0 = mBindingComponent.null.getSelectedDays(daypicker);
This happens when I use two-way data binding: 'app:selectedDays="#={viewModel.treatment.selected_days}"'
I think that is something wrong with #InverseBindingAdapter but I don't know where.
I tried to look for a solution, but unfortunately I couldn't find it. I don't know what should I do to not have null in mBindingComponent object.
I was just doing a workaround and I found solution.
I changed two way data binding to binding and I want in action manually get data from picker and replace in ViewModels data object.
When I do that and I run app in this fragment I got error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.package.android, PID: 12221
java.lang.IllegalStateException: Required DataBindingComponent is null in class FragmentAddStep2BindingImpl. A BindingAdapter in
com.package.utils.textview.DayPicker is not static and requires an
object to use, retrieved from the DataBindingComponent. If you don't
use an inflation method taking a DataBindingComponent, use
DataBindingUtil.setDefaultComponent or make all BindingAdapter methods
static.
at androidx.databinding.ViewDataBinding.ensureBindingComponentIsNotNull(ViewDataBinding.java:709)
at com.package.android.databinding.FragmentAddStep2BindingImpl.(FragmentAddStep2BindingImpl.java:51)
at com.package.android.databinding.FragmentAddStep2BindingImpl.(FragmentAddStep2BindingImpl.java:38)
at com.package.android.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:222)
After that I modify my DayPicker class and I moved adapters of class outside. And finally compilation not show error! Functions in class are not properly implement so don't suggested in.
var days: MutableSet<DayOfWeek> = HashSet()
#BindingAdapter("selectedDays")
fun setSelectedDays(dayPicker: DayPicker, selectedDays: String?) {
if(selectedDays.isNullOrEmpty()){
return
}
days = (selectedDays?.split(",")?.map { id -> DayOfWeek.of(Integer.parseInt(id)) }?.toSet()
?: HashSet()) as MutableSet<DayOfWeek>
}
#InverseBindingAdapter(attribute = "selectedDays")
fun getSelectedDays(dayPicker: DayPicker): String {
if (days.isEmpty()) {
dayPicker.error = "emptyy"
}
return days.map { x -> x.value }.joinToString(",")
}
#BindingAdapter("selectedDaysAttrChanged")
fun setSelectedDaysChangedListener(dayPicker: DayPicker, listener: InverseBindingListener) {
dayPicker
listener.onChange()
}
class DayPicker : TextInputLayout {
private lateinit var tMon: ToggleButton
private lateinit var tTue: ToggleButton
private lateinit var tWed: ToggleButton
private lateinit var tThu: ToggleButton
private lateinit var tFri: ToggleButton
private lateinit var tSat: ToggleButton
private lateinit var tSun: ToggleButton
var mContext: Context? = null
constructor(context: Context) : super(context) {
mContext = context
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
mContext = context
initControl(context)
initDays()
initListeners()
}
private fun initListeners() {
initListener(tMon, DayOfWeek.MONDAY)
initListener(tTue, DayOfWeek.TUESDAY)
initListener(tWed, DayOfWeek.WEDNESDAY)
initListener(tThu, DayOfWeek.THURSDAY)
initListener(tFri, DayOfWeek.FRIDAY)
initListener(tSat, DayOfWeek.SATURDAY)
initListener(tSun, DayOfWeek.SUNDAY)
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
) {
mContext = context
}
/**
* Load component XML layout
*/
private fun initControl(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.daypicker, this, true)
// layout is inflated, assign local variables to components
tMon = findViewById(R.id.tMon)!!
tTue = findViewById(R.id.tTue)!!
tWed = findViewById(R.id.tWed)!!
tThu = findViewById(R.id.tThu)!!
tFri = findViewById(R.id.tFri)!!
tSat = findViewById(R.id.tSat)!!
tSun = findViewById(R.id.tSun)!!
}
fun initDays() {
days.forEach { day ->
if (day == DayOfWeek.MONDAY) {
tMon.isChecked = true
} else if (day == DayOfWeek.TUESDAY) {
tTue.isChecked = true
} else if (day == DayOfWeek.WEDNESDAY) {
tWed.isChecked = true
} else if (day == DayOfWeek.THURSDAY) {
tThu.isChecked = true
} else if (day == DayOfWeek.FRIDAY) {
tFri.isChecked = true
} else if (day == DayOfWeek.SATURDAY) {
tSat.isChecked = true
} else if (day == DayOfWeek.SUNDAY) {
tSun.isChecked = true
}
}
}
fun initListener(button: ToggleButton, day: DayOfWeek) {
button.setOnClickListener {
if (button.isChecked) {
days.add(day)
} else {
days.remove(day)
}
}
}
}
HI I m trying show progress bar with text view for some time like . we have to show text view text for 3 second in that 3 second each second we have to display one by one with one ,two and three dot.
expected :
whole textview should show 3 second
Hello . in one second
Hello.. in 2 second
Hello...in 3 second
after that it should be hide .
I have tried with handler but unable to do this .
private fun blink() {
var iCount = 0;
val handler = Handler()
Thread {
val timeToBlink = 1000
try {
Thread.sleep(timeToBlink.toLong())
} catch (e: Exception) {
}
handler.post {
if(iCount==0)
{
iCount = 1
text = "Hello."
}
else if(iCount==1){
iCount = 2
text = "Hello.."
}
else {
Toast.makeText(context,"...",Toast.LENGTH_LONG).show()
iCount=0
text = "Hello..."
}
}
}.start()
}
Please help me with this .
Try this
class MainActivity : AppCompatActivity() {
private lateinit var tvOne: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvOne = findViewById<TextView>(R.id.tvOne)
}
override fun onResume() {
super.onResume()
tvOne.text = "Hello"
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = "Hello."
}, 1000)
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = "Hello.."
}, 2000)
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = "Hello..."
}, 3000)
Handler(Looper.getMainLooper()).postDelayed({
tvOne.visibility = View.GONE
}, 4000)
}
}
And the XML
<TextView
android:id="#+id/tvOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
TRY This once.
STEP 1 Create class TextAndAnimationView
class TextAndAnimationView : LinearLayout {
lateinit var textToShow: TextView
lateinit var animatedTextView: DotAnimatedTextView
constructor(context: Context) : super(context) {
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
showTextAndAnimation(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
showTextAndAnimation(context, attrs)
}
private fun showTextAndAnimation(context: Context, attrs: AttributeSet) {
inflate(context, R.layout.layout, this)
textToShow = this.findViewById(R.id.text_to_show)
animatedTextView = this.findViewById(R.id.progress_dots_txt)
val ta = context.obtainStyledAttributes(attrs, R.styleable.TextAndAnimationView, 0, 0)
try {
val text = ta.getText(R.styleable.TextAndAnimationView_setText)
val textHint = ta.getText(R.styleable.TextAndAnimationView_setTextHint)
val color = ta.getInt(R.styleable.TextAndAnimationView_setTextColor, 0)
val textSize = ta.getFloat(R.styleable.TextAndAnimationView_setTextSize, 0f)
val dotsCount = ta.getInt(R.styleable.TextAndAnimationView_numberOfDots, 0)
if (text != null)
setText(text)
if (textHint != null)
setTextHint(textHint)
if (color != 0)
setTextColor(color)
if (textSize != 0f)
setTextSize(textSize)
if (dotsCount != 0)
noOfDots(dotsCount)
} finally {
ta.recycle()
}
animatedTextView.showDotsAnimation()
}
fun setText(text: CharSequence) {
textToShow.text = text
}
fun setTextSize(size: Float) {
textToShow.textSize = size
animatedTextView.textSize = size
}
fun setTextHint(textHint: CharSequence) {
textToShow.setHint(textHint)
}
fun setTextColor(color: Int) {
textToShow.setTextColor(color)
animatedTextView.setTextColor(color)
}
fun stopAnimation() {
animatedTextView.stopAnimation()
}
fun noOfDots(dotsCount: Int) {
animatedTextView.noOfDots(dotsCount)
}
fun animationDelay(animationDelayTime: Long) {
animatedTextView.animationDelay(animationDelayTime)
}
}
STEP 2- Create layout.xml in res/layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/text_to_show"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="end|center"
android:text="Progress"
android:textSize="12sp" />
<com.DotAnimatedTextView //ADD YOU Package NAME
android:id="#+id/progress_dots_txt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
android:layout_toRightOf="#id/text_to_show"
android:gravity="start|bottom"
android:maxLines="1"
android:text="..." />
</LinearLayout>
STEP 3 Create attrs.xml in res/values
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TextAndAnimationView">
<attr name="setText" format="string"/>
<attr name="setTextSize" format="float"/>
<attr name="setTextHint" format="string"/>
<attr name="setTextColor" format="integer"/>
<attr name="numberOfDots" format="integer"/>
</declare-styleable>
</resources>
STEP 4 Create class- TextAndAnimationView
class TextAndAnimationView : LinearLayout {
lateinit var textToShow: TextView
lateinit var animatedTextView: DotAnimatedTextView
constructor(context: Context) : super(context) {
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
showTextAndAnimation(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
showTextAndAnimation(context, attrs)
}
private fun showTextAndAnimation(context: Context, attrs: AttributeSet) {
inflate(context, R.layout.layout, this)
textToShow = this.findViewById(R.id.text_to_show)
animatedTextView = this.findViewById(R.id.progress_dots_txt)
val ta = context.obtainStyledAttributes(attrs, R.styleable.TextAndAnimationView, 0, 0)
try {
val text = ta.getText(R.styleable.TextAndAnimationView_setText)
val textHint = ta.getText(R.styleable.TextAndAnimationView_setTextHint)
val color = ta.getInt(R.styleable.TextAndAnimationView_setTextColor, 0)
val textSize = ta.getFloat(R.styleable.TextAndAnimationView_setTextSize, 0f)
val dotsCount = ta.getInt(R.styleable.TextAndAnimationView_numberOfDots, 0)
if (text != null)
setText(text)
if (textHint != null)
setTextHint(textHint)
if (color != 0)
setTextColor(color)
if (textSize != 0f)
setTextSize(textSize)
if (dotsCount != 0)
noOfDots(dotsCount)
} finally {
ta.recycle()
}
animatedTextView.showDotsAnimation()
}
fun setText(text: CharSequence) {
textToShow.text = text
}
fun setTextSize(size: Float) {
textToShow.textSize = size
animatedTextView.textSize = size
}
fun setTextHint(textHint: CharSequence) {
textToShow.setHint(textHint)
}
fun setTextColor(color: Int) {
textToShow.setTextColor(color)
animatedTextView.setTextColor(color)
}
fun stopAnimation() {
animatedTextView.stopAnimation()
}
fun noOfDots(dotsCount: Int) {
animatedTextView.noOfDots(dotsCount)
}
fun animationDelay(animationDelayTime: Long) {
animatedTextView.animationDelay(animationDelayTime)
}
}
STEP 5 - Add this code in your XML code
<?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"
tools:context=".MainActivity">
<com.TextAndAnimationView //ADD YOU Package name first
android:id="#+id/bmi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
STEP 6 Add in your Activity class
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.bmi.setText("HELLO")
}
override fun onStop() {
super.onStop()
binding.bmi.stopAnimation()
}
}
This is the simplest and easiest way to achieve this.
Run this function where you want this will add a new . (dot) every second to TextView. And if the you want to terminate the process just break the operation as shown in condition
fun addDotEverySecond() {
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = tvOne.text + "."
//condition to check needs to terminate or not.
if(your condition here){
// just return from here to terminate this.
}else{
addDotEverySecond()
}
}, 1000)
}
I'm developing an app with server-localized strings, and I want to use this localized string across my entire app.
So I came up with the following approach and would like to know if it will cause me any issues, or is there any better practice I can follow.
My Approach:
Let my android Application class implements Localizer interface defined as follows:
interface Localizer {
// get the localized string version for a given string key
fun getLocalizedValue(key: String): String?
}
class MyApplication : Application(), Localizer {
var localizedStrings:Map<String,String> = emptyMap()
override fun getLocalizedValue(key: String): String? =
localizedStrings[key]
}
In Splash Logic: fetch my localized strings from server, and fill localizedStrings with the fetched (key,value) items.
And then: use MyApplication class as a Localizer object across the app.
Following is a sample code for a custom LocalizedTextView that utilizes the 'Localizer' interface the way I mentioned:
class LocalizedTextView : AppCompatTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initAttrs(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,
attrs,
defStyleAttr) {
initAttrs(attrs)
}
private fun initAttrs(attrs: AttributeSet?) {
if (attrs == null) return
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.LocalizedTextView)
val textKey = styledAttrs.getText(R.styleable.LocalizedTextView_localized_text)
text =
if (context.applicationContext is Localizer)
(context.applicationContext as Localizer).getLocalizedValue(textKey.toString())
else textKey.toString()
styledAttrs.recycle()
}
}
I'd like to make my app displays texts one by one, with typewriter effect. I made a custom view for the effect. When the first custom view finishes displaying the texts, my app would wait for a click on the screen, or the layout. On the click, the next custom view starts to display texts, and when it finishes it waits for a click, and so on.
This is the custom view I made:
class TypeWriterView: AppCompatTextView {
private var mText: CharSequence? = null
private var mIndex = 0
private var mDelay: Long = 150 // in ms
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private val mHandler: Handler = Handler()
private val characterAdder: Runnable = object : Runnable {
override fun run() {
text = mText!!.subSequence(0, mIndex++)
if (mIndex <= mText!!.length) {
mHandler.postDelayed(this, mDelay)
}
}
}
fun commence(txt: CharSequence?, delay: Long) {
mText = txt
mIndex = 0
text = ""
mDelay = delay
mHandler.removeCallbacks(characterAdder)
mHandler.postDelayed(characterAdder, mDelay)
}
}
To make it work like I mentioned above, I thought it would need to pass some values to activity(in my case it's actually a fragment) on finish, so I searched some and put some code in run() method.
override fun run() {
val job = GlobalScope.launch(Dispatchers.Default) {
text = mText!!.subSequence(0, mIndex++)
if (mIndex <= mText!!.length) {
mHandler.postDelayed(**this**, mDelay)
}
}
runBlocking {
job.join()
// pass value after finishing to display texts
}
}
Then "this" gets red underline with the error message saying:
Type mismatch.
Required: Runnable
Found: CoroutineScope
So what should I put here instead of "this"? Or are there any better way to do this?
The Handler is of no use when you are using coroutines.
The delay() should be used to delay the execution, and you don't need to push Runnable to the thread again and again.
class TypeWriterView: AppCompatTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
suspend fun commence(txt: CharSequence?, delayMs: Long) {
if (txt == null) return
var index = 0
while (index < txt.length) {
text = txt.subSequence(0, ++index)
delay(delayMs)
}
}
// Get [Job] reference after launching in [owner]
fun commenceOnLifecycle(owner: LifecycleOwner, txt: CharSequence?, delayMs: Long): Job =
owner.lifecycleScope.launch { commence(txt, delayMs) }
}
LifecycleOwner is implemented by Activity, Fragment, etc. whichever has a lifecycle attached (more info: LifecycleOwner), it dispatches job to Dispatchers.Main by default so UI can be changed.
I have created below class to set the custom fonts or any otf fonts in my normal EditText.
Am working with kotlin and created below class for the generalization of using CustomEditText.
I have added fonts file in asset and created below class named CustomEditText:
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Typeface
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatEditText
class CustomEditText : AppCompatEditText{
constructor(context: Context) : this(context, null) {
init(null)
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) {
init(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(attrs)
}
private fun init(attrs: AttributeSet?) {
CustomEditText(context, attrs, 0)
}
/**
* #param context:This is an abstract class whose implementation is provided by Android Operating System.
* #param attrs:A collection of attributes, as found associated with a tag in an XML document.
* #param defStyle:
*/
fun CustomEditText(
context: Context,
attrs: AttributeSet?,
defStyle: Int
) {
try {
val a: TypedArray =
context.obtainStyledAttributes(attrs, R.styleable.CustomEditText, defStyle, 0)
val customEnumValue: CustomEnumBrandonNew.CustomFontType =
CustomEnumBrandonNew.CustomFontType.fromId(
a.getInt(
R.styleable.CustomEditText_font_type,
0
)
)
a.recycle()
typeface = when (customEnumValue) {
CustomEnumBrandonNew.CustomFontType.BLACK ->
Typeface.createFromAsset(context.assets, "BrandonText-Black.otf") // BrandonTextBlack().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.BLACK_ITALIC ->
Typeface.createFromAsset(context.assets, "BrandonText-BlackItalic.otf") // BrandonTextBlackItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.BOLD ->
Typeface.createFromAsset(context.assets, "BrandonText-Bold.otf") // BrandonTextBold().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.BOLD_ITALIC ->
Typeface.createFromAsset(context.assets, "BrandonText-BoldItalic.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.LIGHT ->
Typeface.createFromAsset(context.assets, "BrandonText-Light.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.LIGHT_ITALIC ->
Typeface.createFromAsset(context.assets, "BrandonText-LightItalic.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.MEDIUM ->
Typeface.createFromAsset(context.assets, "BrandonText-Medium.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.MEDIUM_ITALIC ->
Typeface.createFromAsset(context.assets, "BrandonText-MediumItalic.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.REGULAR ->
Typeface.createFromAsset(context.assets, "BrandonText-Regular.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.REGULAR_ITALIC ->
Typeface.createFromAsset(context.assets, "BrandonText-RegularItalic.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.THIN ->
Typeface.createFromAsset(context.assets, "BrandonText-Thin.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
CustomEnumBrandonNew.CustomFontType.THIN_ITALIC ->
Typeface.createFromAsset(context.assets, "BrandonText-ThinItalic.otf") // BrandonTextBoldItalic().getInstance(context)?.getTypeFace()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
Before it, I have also created seperate classes i.e. BrandonTextBlack, BrandonTextBlackItalic, BrandonTextBold etc. as below :
class BrandonTextBold{
private var instance: BrandonTextBold? = null
private var typeface: Typeface? = null
fun getInstance(context: Context): BrandonTextBold? {
synchronized(BrandonTextBold::class.java) {
if (instance == null) {
typeface = Typeface.createFromAsset(context.resources.assets, "BrandonText-Bold.otf")
}
return instance
}
}
fun getTypeFace(): Typeface? {
return typeface
}
}
Now, in layout xml file, using it as below :
<CustomEditText
android:id="#+id/edt_mobile_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="#dimen/dimen_40"
android:background="#drawable/drawable_round_button_transperent"
android:hint="#string/str_hint_enter_your_mobile_number"
android:imeOptions="actionDone"
android:inputType="number|phone"
android:maxLength="10"
android:textColorHint="#color/colorHintAndBorder"
android:maxLines="1"
android:paddingBottom="#dimen/dimen_8"
android:paddingTop="#dimen/dimen_8"
android:paddingRight="#dimen/dimen_15"
android:paddingLeft="#dimen/dimen_15"
android:textColor="#color/colorBlack"
android:textSize="#dimen/font_dimen_20"
app:font_type="regular" />
Here notice that I have used app:font_type="regular". So, I can have normal font type in my edittext. It's done.
But, the issue is softkeyboard not opening when I press or try to write something in it.
What might be the issue?
Use default style as EditText style in your all constructors instead of 0
From
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) {
init(attrs)
}
To
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, android.R.attr.editTextStyle) {
init(attrs)
}