Custom View: Strange behavior on configuration changes [duplicate] - android

This question already has answers here:
All Editexts be replace by same value after pop a Fragment
(4 answers)
Closed 4 years ago.
I have a custom view with an edittext and an imageview. I use this view multiple times in the same layout. It's behaving very strange though. Whenever I rotate the device the value from the last declared view in the xml is put into all the other views in the layout and I just can't figure out where things go wrong.
My view:
class InputView(context: Context, attrs: AttributeSet?, #AttrRes defStyleAttr: Int) : ConstraintLayout(context, attrs, defStyleAttr) {
private var textInputLayout: TextInputLayout
private var textView: TextInputEditText
private var imageView: ImageView
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
init {
val view = LayoutInflater.from(context).inflate(R.layout.custom_inputview, this, true)
textInputLayout = view.custom_inputview_text_input_layout
textView = view.custom_inputview_text_input_edit_text
imageView = view.custom_inputview_image_view
attrs.let {
context.theme.obtainStyledAttributes(
it,
R.styleable.InputView,
defStyleAttr, 0).apply {
try {
textView.textSize = getDimension(R.styleable.InputView_android_textSize, 16.0f)
textView.text = SpannableStringBuilder(getString(R.styleable.InputView_android_text) ?: "")
textInputLayout.hint = getText(R.styleable.InputView_android_hint)
if (getDrawable(R.styleable.InputView_android_src) == null) {
imageView.isVisible = false
} else {
imageView.setImageDrawable(getDrawable(R.styleable.InputView_android_src))
imageView.setColorFilter(getColorOrThrow(R.styleable.InputView_android_tint))
}
textView.maxLines = getInteger(R.styleable.InputView_android_maxLines, 1)
textView.minLines = getInteger(R.styleable.InputView_android_minLines, 0)
textView.setLines(getInteger(R.styleable.InputView_android_lines, 1))
textView.inputType = getInteger(R.styleable.InputView_android_inputType, EditorInfo.IME_NULL)
textView.setCompoundDrawablesWithIntrinsicBounds(
getDrawable(R.styleable.InputView_android_drawableStart),
getDrawable(R.styleable.InputView_android_drawableTop),
getDrawable(R.styleable.InputView_android_drawableEnd),
getDrawable(R.styleable.InputView_android_drawableBottom))
} finally {
recycle()
}
}
}
}
override fun onSaveInstanceState(): Parcelable {
// 1
val bundle = Bundle()
// 2
bundle.putString("text", textView.text.toString())
// 3
Log.d("InputView", "Saving state text: ${textView.text.toString()}")
bundle.putParcelable("superState", super.onSaveInstanceState())
return bundle
}
override fun onRestoreInstanceState(state: Parcelable) {
// 4
var viewState = state
if (viewState is Bundle) {
// 5
Log.d("InputView", "Textview text: ${viewState.getString("text")}")
textView.text = SpannableStringBuilder(viewState.getString("text"))
// 6
viewState = viewState.getParcelable("superState")
}
super.onRestoreInstanceState(viewState)
}
}
I've tried overriding onSaveIntanceState() and onRestoreInstanceState(), but it doesn't change anything.
Screenshot before rotation:
Screenshot after rotation:

Added this to my custom view and the problem was gone:
class InputView(context: Context, attrs: AttributeSet?, #AttrRes defStyleAttr: Int) : ConstraintLayout(context, attrs, defStyleAttr) {
...
#Suppress("UNCHECKED_CAST")
public override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
val ss = SavedState(superState)
ss.childrenStates = SparseArray()
for (i in 0 until childCount) {
getChildAt(i).saveHierarchyState(ss.childrenStates as SparseArray<Parcelable>)
}
return ss
}
#Suppress("UNCHECKED_CAST")
public override fun onRestoreInstanceState(state: Parcelable) {
val ss = state as SavedState
super.onRestoreInstanceState(ss.superState)
for (i in 0 until childCount) {
getChildAt(i).restoreHierarchyState(ss.childrenStates as SparseArray<Parcelable>)
}
}
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
class SavedState(superState: Parcelable?) : BaseSavedState(superState) {
var childrenStates: SparseArray<Any>? = null
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
childrenStates?.let {
out.writeSparseArray(it)
}
}
}
}

Related

Can somebody help me custom checkbox to Read aloud with format : checkbox + [text] + [state of checkbox]?

Is there a way to custom checkbox to Read aloud with format : checkbox + [text] + [state of checkbox] that the screen reader, in android case the TalkBack, has to read the accessible elements ?
CODE:
class CustomRadioButton : AppCompatRadioButton {
var talkBackString: CharSequence? = null
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
) {
setupView()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setupView()
}
constructor(context: Context) : super(context) {
setupView()
}
private fun setupView() {
accessibilityDelegate = MyAccessibilityDelegate()
}
inner class MyAccessibilityDelegate : AccessibilityDelegate() {
/**
* Override function onInitializeAccessibilityNodeInfo of AccessibilityDelegate
* #param host is [View]
* #param info is [AccessibilityNodeInfo]
*/
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(host, info)
if (talkBackString.isNullOrEmpty() && host.contentDescription.isNullOrEmpty().not()) {
talkBackString = host.contentDescription
}
info.apply {
contentDescription = ""
hintText = talkBackString.toString()
error = ""
text = ""
}
}
}
}

AutoCompleteTextView - when update adapter, dropdown auto close

I use MaterialAutoCompleteTextView, when i add new item / items and call notifyDataSetChanged(), dropdown automatically closes
How to fix this
VIDEO:
https://drive.google.com/file/d/1AYWW-_HcIpefrn0RcPMx2EsLLoyB2TtY/view?usp=sharing
CODE:
class MainActivity : AppCompatActivity() {
private lateinit var materialAutoComplete: MaterialAutoCompleteTextView
private lateinit var adapter: CustomArrayAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
materialAutoComplete = findViewById(R.id.materialAutoComplete_main)
setupMaterialAutoComplete()
}
private var number = 0
private fun setupMaterialAutoComplete() {
val items = mutableListOf("Shop", "Shop", "Shop", "Shop")
adapter = CustomArrayAdapter(
this,
R.layout.item_goods_material_auto_complete,
items
)
materialAutoComplete.setAdapter(adapter)
object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
number += 1
adapter.addAll(mutableListOf("$number", "${number + 1}"))
}
}.start()
}
}
Just override this methods
class MaterialAutoCompleteTextViewWithoutAutoCloseDropdown : MaterialAutoCompleteTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet)
constructor(
context: Context, attributeSet: AttributeSet?, defStyleAttr: Int,
) : super(context, attributeSet, defStyleAttr)
override fun dismissDropDown() {
}
override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing) {
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
val state = keyDispatcherState
state?.startTracking(event, this)
return true
} else if (event.action == KeyEvent.ACTION_UP) {
val state = keyDispatcherState
state?.handleUpEvent(event)
if (event.isTracking && !event.isCanceled) {
super.dismissDropDown()
return true
}
}
}
return super.onKeyPreIme(keyCode, event)
}
override fun replaceText(text: CharSequence?) {
clearComposingText()
setText(text)
val spannable = getText()
Selection.setSelection(spannable, spannable.length)
super.dismissDropDown()
}
}

Android Nested RecyclerView Change Angle Of Scroll

I have horizontal recyclerViews in vertical RecyclerView. To scroll horizontally I should swipe from 45 degrees to -45 degree. To scroll vertically from 90 degrees to 45 degrees. Is any possibility to override this?
For example, vertical scroll works only with the swipe from 105 to 75 degrees. Else, horizontal scroll.
You need to create MainVerticalRecyclerView like this:
open class MainVerticalRecyclerView : RecyclerView {
private var scrollPointerId = -1
private var pointTouchX = 0
private var pointTouchY = 0
private var touchSlopType = 0
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val vc = ViewConfiguration.get(context)
touchSlopType = vc.scaledTouchSlop
}
override fun setScrollingTouchSlop(slopConstant: Int) {
super.setScrollingTouchSlop(slopConstant)
val vc = ViewConfiguration.get(context)
when (slopConstant) {
TOUCH_SLOP_DEFAULT -> touchSlopType = vc.scaledTouchSlop
TOUCH_SLOP_PAGING -> touchSlopType = vc.scaledPagingTouchSlop
}
}
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
if (e == null) {
return false
}
val action = e.actionMasked
val actionIndex = e.actionIndex
when (action) {
MotionEvent.ACTION_DOWN -> {
scrollPointerId = e.getPointerId(0)
pointTouchX = Math.round(e.x + 0.5f)
pointTouchY = Math.round(e.y + 0.5f)
return super.onInterceptTouchEvent(e)
}
MotionEvent.ACTION_POINTER_DOWN -> {
scrollPointerId = e.getPointerId(actionIndex)
pointTouchX = Math.round(e.getX(actionIndex) + 0.5f)
pointTouchY = Math.round(e.getY(actionIndex) + 0.5f)
return super.onInterceptTouchEvent(e)
}
MotionEvent.ACTION_MOVE -> {
val index = e.findPointerIndex(scrollPointerId)
if (index < 0) {
return false
}
val x = Math.round(e.getX(index) + 0.5f)
val y = Math.round(e.getY(index) + 0.5f)
if (scrollState != SCROLL_STATE_DRAGGING) {
val dx = x - pointTouchX
val dy = y - pointTouchY
var startScroll = false
if (layoutManager?.canScrollHorizontally() == true && Math.abs(dx) > touchSlopType && (layoutManager?.canScrollVertically() == true || Math.abs(dx) > Math.abs(dy))) {
startScroll = true
}
if (layoutManager?.canScrollVertically() == true && Math.abs(dy) > touchSlopType && (layoutManager?.canScrollHorizontally() == true || Math.abs(dy) > Math.abs(dx))) {
startScroll = true
}
return startScroll && super.onInterceptTouchEvent(e)
}
return super.onInterceptTouchEvent(e)
}
else -> {
return super.onInterceptTouchEvent(e)
}
}
}
}
and create ChildHorizontalRecyclerView for other nested RecyclerView inside main RecyclerView like this:
class ChildHorizontalRecyclerView : MainVerticalRecyclerView {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
}
if you want scroll like in PlayStore when you touch HorizontalChildRecyclerView need write:
recyclerView.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN && rv.scrollState == RecyclerView.SCROLL_STATE_SETTLING) {
rv.stopScroll()
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
})

Memory Leak in Custom Textview with Spannable Android

I have one custom textview which has five city names and spannable click listener for each one of them. Code is as follow
/**
* Created by #raj on 26/04/18.
*/
class TopCitiesTextView : TextView {
private var mListener: OnCityClickListener? = null
constructor(ctx: Context?) : super(ctx) {
initView(ctx, null, 0)
}
constructor(ctx: Context?, attrs: AttributeSet?) : super(ctx, attrs) {
initView(ctx, attrs, 0)
}
constructor(ctx: Context?, attrs: AttributeSet?, defStyle: Int) : super(ctx, attrs, defStyle) {
initView(ctx, attrs, defStyle)
}
private fun initView(ctx: Context?, attrs: AttributeSet?, defStyle: Int) {
}
override fun onFinishInflate() {
super.onFinishInflate()
val cityArray: Array<out String> = context.resources.getStringArray(R.array.top_cities_view_text)
val spannableString: SpannableString = SpannableString(cityArray[0])
this.text = spannableString
this.append(" ")
for (i in 1 until cityArray.size) {
val citySpannableString = SpannableString(cityArray[i])
citySpannableString.setSpan(object : ClickableSpan() {
override fun onClick(widget: View?) {
mListener?.onCityClicked(cityArray[i])
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}, 0, citySpannableString.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
this.append(citySpannableString)
if (i != cityArray.size-1)
this.append(", ")
}
movementMethod = LinkMovementMethod.getInstance()
}
fun setCityClickListener(listener: OnCityClickListener) {
this.mListener = listener
}
interface OnCityClickListener {
fun onCityClicked(city: String?)
}
}
But the issue is that I am getting memory leak while using this textview in fragment's layout(xml) file.
Here is the screenshot of leakcanary.
How to remove this memory leak?
Remove the ClickableSpan from text in onDestroy of your Activity to avoid the Leak.
if (textView.getText() instanceof SpannableString) {
SpannableString spannableStr = (SpannableString) textView.getText();
ClickableSpan[] spans = spannableStr.getSpans(0, spannableStr.length(), ClickableSpan.class);
for (ClickableSpan span : spans) {
spannableStr.removeSpan(span);
}
textView.setText(spannableStr);
}

LinearLayout is not drawing though onDraw is called

I'm building an MultiStateToggle Button.
To highlight the selected state, I need to draw a rectangle which is exactly the size of a button. This rectangle slides to the current state. Therefore I need to draw, but whatever I do, I do not see any change even though onDraw was called.
The superclass ToggleButton simply holds some constants. It extends LinearLayout!
Does anybody know, why drawing does not work in this LinearLayout sub-class?
class MultiStateToggleButton : ToggleButton {
//region Variables
private val mHighlightPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
#DrawableRes
private val iSelectableItemBackgroundDrawableResId: Int
private val mButtons = mutableListOf<Button>()
//endregion
constructor(context: Context) : this(context, Any() as AttributeSet)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
val mTypedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorPrimary, mTypedValue, true)
val iPrimaryColor = mTypedValue.data
context.theme.resolveAttribute(R.attr.colorPrimary, mTypedValue, true)
val iPrimaryDarkColor = mTypedValue.data
setBackgroundColor(iPrimaryDarkColor)
context.theme.resolveAttribute(R.attr.colorAccent, mTypedValue, true)
val iAccentColor = mTypedValue.data
val mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.MultiStateToggleButton)
foregroundColor = mTypedArray.getColor(R.styleable.MultiStateToggleButton_foregroundColor, iPrimaryColor)
highlightColor = mTypedArray.getColor(R.styleable.MultiStateToggleButton_highlightColor, iAccentColor)
val mTexts = mTypedArray.getTextArray(R.styleable.MultiStateToggleButton_elements)
if (mTexts != null) {
val iSelectedElement = mTypedArray.getInt(R.styleable.MultiStateToggleButton_selectedElement, 0)
setElements(Array(mTexts.size) { i ->
mTexts[i].toString()
}, iSelectedElement)
}
mTypedArray.recycle()
}
init {
val mTypedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, mTypedValue, true)
iSelectableItemBackgroundDrawableResId = mTypedValue.resourceId
mHighlightPaint.apply {
color = highlightColor
style = Paint.Style.FILL
}
}
//region Public Variables
override var selectedElement: Int = 0
set(value) {
selectButton(value)
field = value
super.selectedElement = value
}
//endregion
//region Methods
fun setElements(texts: Array<String>) {
removeAllViews()
mButtons.clear()
texts.forEachIndexed { i, text ->
val mButton = Button(context).apply {
layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, LAYOUT_WEIGHT_EQUAL)
setBackgroundResource(iSelectableItemBackgroundDrawableResId)
setText(text)
setTextColor(textColor)
setOnClickListener { selectedElement = i }
}
mButtons.add(mButton)
addView(mButton)
}
}
fun setElements(texts: Array<String>, selectedItem: Int) {
setElements(texts)
selectButton(selectedItem)
}
override fun setEnabled(enabled: Boolean) {
for (i in 0 until childCount) {
getChildAt(i).isEnabled = enabled
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawRect(0f, 0f, 30f, 30f, mHighlightPaint)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
setWillNotDraw(false)
}
override fun onSaveInstanceState(): Parcelable {
return Bundle().apply {
putParcelable(KEY_INSTANCE_STATE, super.onSaveInstanceState())
putInt(KEY_SELECTED_BUTTON, selectedElement)
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
var mRestoredState: Parcelable? = state
if (state is Bundle) {
selectButton(state.getInt(KEY_SELECTED_BUTTON))
mRestoredState = state.getParcelable(KEY_INSTANCE_STATE)
}
super.onRestoreInstanceState(mRestoredState)
}
private fun selectButton(position: Int) {
if (mButtons.isNotEmpty()) {
if (position >= mButtons.size) {
throw IndexOutOfBoundsException("Position was $position but there are only ${mButtons.size} Buttons.")
}
getChildAt(selectedElement).isEnabled = true
getChildAt(position).isEnabled = false
invalidate()
}
}
//endregion
companion object {
private const val KEY_SELECTED_BUTTON = "mtb_selected_button"
private const val KEY_INSTANCE_STATE = "mtb_instance_state"
}
}
As Kotlins' init block is called on Primary Constructor call, my Paint was not initialized.

Categories

Resources