I have the following AutoCompleteTextView:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/offering_type_dropdown_layout"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/date_card_spacing"
android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="#+id/offering_details_header_image"
app:layout_constraintEnd_toStartOf="#+id/offering_details_date_layout"
app:layout_constraintTop_toTopOf="parent"
android:hint="#string/offering_type_hint">
<AutoCompleteTextView
android:id="#+id/offering_details_type_dropdown"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textNoSuggestions"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:cursorVisible="false"/>
</com.google.android.material.textfield.TextInputLayout>
In my Activity's onCreate, I fill the AutoCompleteTextView like this:
String[] TYPES = new String[] {getString(R.string.burnt_offering), getString(R.string.meal_offering), getString(R.string.peace_offering), getString(R.string.sin_offering)};
ArrayAdapter<String> adapter = new ArrayAdapter<>(OfferingInputActivity.this, R.layout.offering_types_dropdown, TYPES);
mOfferingTypeCombo.setAdapter(adapter);
Then I populate the view using a Room database and preselect one of the values. In the Room callback, I do:
mOfferingTypeCombo.setText(getString(R.string.meal_offering)), false);
Everything works well on the initial run, and the dropdown is shown correctly:
Now I rotate the device to landscape. The very same code as above is executed but this time, the dropdown box only shows the current selection:
For some reason, all other entries in the adapter have disappeared. I have tried hacks such as setAdapter(null) before I set the adapter, but no success. Can someone tell me why after rotation, the dropdown is missing entries even though the exact same code is executed?
Currently there is a open bug on this topic.
You can use as workaround the setFreezesText method:
AutoCompleteTextView autoCompleteTextView =
view.findViewById(R.id.offering_details_type_dropdown);
autoCompleteTextView.setFreezesText(false);
The EditText set the freezesText=true. Due to this value after the rotation the TextView#onRestoreInstanceState(Parcelable) calls autoCompleteTextView.setText(value,true) which applies a filter to the adapter values.
This custom MaterialAutoCompleteTextView
resolves all problems:
class ExposedDropdownMenu : MaterialAutoCompleteTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun getFreezesText(): Boolean {
return false
}
init {
inputType = InputType.TYPE_NULL
}
override fun onSaveInstanceState(): Parcelable? {
val parcelable = super.onSaveInstanceState()
if (TextUtils.isEmpty(text)) {
return parcelable
}
val customSavedState = CustomSavedState(parcelable)
customSavedState.text = text.toString()
return customSavedState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is CustomSavedState) {
super.onRestoreInstanceState(state)
return
}
setText(state.text, false)
super.onRestoreInstanceState(state.superState)
}
private class CustomSavedState(superState: Parcelable?) : BaseSavedState(superState) {
var text: String? = null
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeString(text)
}
}
}
Source
Note: It may not works correctly in older APIs like 23 or below.
one way is using a custom ArrayAdapter that prevents to Filter texts.
class NoFilterArrayAdapter : ArrayAdapter<Any?> {
constructor(context: Context, resource: Int) : super(context, resource)
constructor(context: Context, resource: Int, objects: Array<out Any?>) : super(context, resource, objects)
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults? {
return null
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {}
}
}
}
usage:
val adapter = NoFilterArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, items)
Like #Gabriele Mariotti mentioned, it's a bug. As mentioned in the posted link, I did this workaround which works well:
public class ExposedDropDown extends MaterialAutoCompleteTextView {
public ExposedDropDown(#NonNull final Context context, #Nullable final AttributeSet attributeSet) {
super(context, attributeSet);
}
#Override
public boolean getFreezesText() {
return false;
}
}
I solved this by deleting id from AutoCompleteTextView. This id is responsible for saving text after rotating.
Save string from AutoCompleteTextView in onSaveInstanceState method.
Code:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/inputAddress"
style="#style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/Address">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
/>
</com.google.android.material.textfield.TextInputLayout>
list_item.xml
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceSubtitle1"
/>
fragment.class
class CashierAddFragment : Fragment() {
var mBinding: FragmentCashierAddBinding? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentCashierAddBinding.inflate(inflater, container, false)
if(savedInstanceState == null) {
initAddressSpinner(binding, "")
} else {
initAddressSpinner(binding, savedInstanceState.getString(KEY_ADDRESS))
}
mBinding = binding
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
val address = mBinding!!.inputAddress.getTrimText()
outState.putString(KEY_ADDRESS, address)
super.onSaveInstanceState(outState)
}
private fun initAddressSpinner(binding: FragmentCashierAddBinding, initValue: String?) {
val items = listOf("Option 1", "Option 2", "Option 3", "Option 4")
val adapter = ArrayAdapter(requireContext(), R.layout.list_item, items)
val autoTxtAddress = binding.inputAddress.editText as? AutoCompleteTextView
autoTxtAddress?.setText(initValue)
autoTxtAddress?.setAdapter(adapter)
}
}
Related
I have the following RecycleView adapter:
class PageViewerAdapter(val context: Context, private val pages: List<Page>) :
RecyclerView.Adapter<PageViewerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(context);
val view = inflater.inflate(R.layout.page_viewer_card, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.testView.text = position.toString()
holder.panelView.isEnabled = false
}
override fun getItemCount(): Int {
return pages.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val testView: TextView = itemView.test_text
val panelView: PanelView = itemView.page_preview
}
}
The problem is that itemView.page_preview, which is referencing my custom view in the XML, is null. Both in ViewHolder initialization and in the onBindViewHolder.
This is the RecyclerView card/item XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:id="#+id/test_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<my.package.PanelView
android:id="#+id/page_preview"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintDimensionRatio="W, 1:1.4142"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
If however, the page_preview references are removed from ViewHolder and onBindViewHolder, the views will load fine within a few seconds.
Update
This is the custom view class:
class PanelView:
(context: Context?, attrs: AttributeSet?) : SurfaceView(context), SurfaceHolder.Callback {
private var canvasThread: CanvasThread
init {
this.holder.addCallback(this)
canvasThread = CanvasThread(this.holder, this)
this.isFocusable = true
}
override fun surfaceCreated(holder: SurfaceHolder) {
resume()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
pause()
}
fun resume() {
if (!canvasThread.isAlive) {
canvasThread = CanvasThread(this.holder, this)
canvasThread.run = true
canvasThread.start()
}
}
fun pause() {
if (canvasThread.isAlive) {
var retry = true
canvasThread.run = false
while (retry) {
try {
canvasThread.join()
retry = false
} catch (e: InterruptedException) {
}
}
}
}
}
You are probably using kotlinx.android.synthetic import statements to access your specific within the parent view. This does not work for recycler views, instead you should do val panelView = itemView.findViewById(R.id.page_preview)
Kotlinx synthetic doesn't work by default in RecyclerView.ViewHolder class, you need to have it extend kotlinx.android.extensions.LayoutContainer:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {
val testView: TextView = itemView.test_text //now this will work
val panelView: PanelView = itemView.page_preview
}
Anyway, you should really be careful with syntetic imports as they can be a headache to debug, especially in fragments. You might as well just use the fancy new findViewById<View>
Edit 1:
Since you've posted your custom view class, at first glance it seems that the super constructor invocation isn't right. You could use the following for all custom view kotlin classes:
class CustomView #JvmOverloads constructor(
ctx: Context,
attrs: AttributeSet? = null,
defStyleAttrs: Int = 0
) : View(ctx, attrs, defStyleAttrs)
How to save scroll state of scrollview properly.In my code, I'm using :
scroll_x = scrollView.getScrollX();
scroll_y = scrollView.getScrollY();
when activity pause,i'm stored x and y as you can see here, and when activity start, i'm scroll scrollView to x and y.
But crux is (main problem) is, scrollview not scrollview to x and y properly, it scroll up or down a little bit automatically. How to fix it?
You can manage the instance state by using this class:
class SaveScrollNestedScrollViewer : NestedScrollView {
constructor(context: Context) : super(context)
constructor(context: Context, attributes: AttributeSet) : super(context, attributes)
constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) : super(context, attributes, defStyleAttr)
public override fun onSaveInstanceState(): Parcelable? {
return super.onSaveInstanceState()
}
public override fun onRestoreInstanceState(state: Parcelable?) {
super.onRestoreInstanceState(state)
}
}
use on your xml:
<yourClassNamePlace.SaveScrollNestedScrollViewer
android:id="#+id/my_scroll_viewer"
android:layout_width="match_parent"
android:layout_height="match_parent">
</yourClassNamePlace.SaveScrollNestedScrollViewer>
and then use in activity like this:
class MyActivity : AppCompatActivity() {
companion object {
var myScrollViewerInstanceState: Parcelable? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity)
if (myScrollViewerInstanceState != null) {
my_scroll_viewer.onRestoreInstanceState(myScrollViewerInstanceState)
}
}
public override fun onPause() {
super.onPause()
myScrollViewerInstanceState = my_scroll_viewer.onSaveInstanceState()
}
}
I want to use a centered spinner where the width of the spinner is only as wide as the selected item text. From my research it seems that this is not natively supported out of the box with an attribute so I found another StackOverflow question/answer and tried implementing that but ran into some issues with it.
So I took option 1 from this SO response and implemented it in Kotlin and It's not working for me
class DynamicWidthSpinner #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatSpinner(context, attrs, defStyleAttr) {
override fun setAdapter(adapter: SpinnerAdapter?) {
super.setAdapter(if (adapter != null) WrapperSpinnerAdapter(adapter) else null)
}
inner class WrapperSpinnerAdapter(val baseAdapter: SpinnerAdapter) : SpinnerAdapter {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return baseAdapter.getView(selectedItemPosition, convertView, parent)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return baseAdapter.getDropDownView(position, convertView, parent)
}
override fun getCount(): Int = baseAdapter.count
override fun getItem(position: Int): Any = baseAdapter.getItem(position)
override fun getItemId(position: Int): Long = baseAdapter.getItemId(position)
override fun getItemViewType(position: Int): Int = baseAdapter.getItemViewType(position)
override fun getViewTypeCount(): Int = baseAdapter.viewTypeCount
override fun hasStableIds(): Boolean = baseAdapter.hasStableIds()
override fun isEmpty(): Boolean = baseAdapter.isEmpty
override fun registerDataSetObserver(observer: DataSetObserver) {
baseAdapter.registerDataSetObserver(observer)
}
override fun unregisterDataSetObserver(observer: DataSetObserver) {
baseAdapter.unregisterDataSetObserver(observer)
}
}
}
and in my MainActivity I'm doing this from onCreate
val spinner: DynamicWidthSpinner = findViewById(R.id.global_toolbar_location_spinner)
val tempLocationList = ArrayList<String>()
tempLocationList.add("Test1")
tempLocationList.add("Much longer test string 2")
spinner.adapter = ArrayAdapter(
this,
R.layout.global_toolbar_spinner_item,
tempLocationList
)
spinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, arg3: Long) {
// TODO: do stuff on selection here
}
override fun onNothingSelected(arg0: AdapterView<*>) {
// TODO: do nothing... yet
}
}
spinner.setSelection(0)
and I am using my custom Spinner in the layout xml (ommitting everything else that is not necessary because I am able to get it work just fine using the native <Spinner> or androidx compat Spinner
<com.blablabla.app.ui.DynamicWidthSpinner
android:id="#+id/global_toolbar_location_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:spinnerMode="dialog"
/>
What I see is just the first item "Test1" and nothing happens when I tap on it and arrow seems to have disappeared now as well
I figured out the issue. Turns out that this is one of those cases where the #JvmOverloads doesn't work. Once I converted it to the multiple constructor kotlin syntax it worked without a problem
class DynamicWidthSpinner : AppCompatSpinner {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
...
}
I am trying to implement some custom bindingadapters so that I can databind my viewmodel value to the switch.ischecked value in my custom view. I want the switch state to change if enabled in my viewmodel changes and visa-versa. I have looked at numerous articles on how to accomplish this, yet it still does nothing. I can see that my setSwitchChecked method is being used by the databinding implementation, but it doesn't seem to actually set anything. the other 2 adapters remain unused. Any help as so what I am missing or doing wrong is appreciated.
ViewModel
open class SettingsViewModel #Inject constructor(): ViewModel() {
var enabled: MutableLiveData<Boolean> = MutableLiveData()
}
Fragment
class SettingsFragment #Inject constructor(): Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewDataBinding: FragmentSettingsBinding
private lateinit var viewModel: SettingsViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_settings, container, false)
viewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(SettingsViewModel::class.java)
viewDataBinding = FragmentSettingsBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return view
}
}
CustomView xml binding
<com.stinson.sleepcycles.views.SwitchRow
android:id="#+id/switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:switchLabel="#string/enabled"
app:switchChecked="#{viewmodel.enabled}"/>
Custom View Class
class SwitchRow constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) :
RelativeLayout(context, attrs, defStyle), View.OnClickListener {
constructor(context: Context, attrs: AttributeSet): this(context, attrs, 0)
init {
val view = inflate(context, R.layout.view_switch_row, this)
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.SwitchRow, defStyle, 0)
try {
view.text_label.text = a.getString(R.styleable.SwitchRow_switchLabel)
view.switch_toggle.isChecked = a.getBoolean(R.styleable.SwitchRow_switchChecked, false)
} finally {
a.recycle()
}
view.setOnClickListener {
view.switch_toggle.callOnClick()
}
view.switch_toggle.setOnClickListener {
toggleSwitch(view)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP) {
this.callOnClick()
}
return super.dispatchTouchEvent(event)
}
override fun onClick(view: View?) {
if (view != null) view.callOnClick()
}
private fun toggleSwitch(view: View) {
view.switch_toggle.isChecked = !view.switch_toggle.isChecked
}
}
Custom View XML
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="16dp">
<TextView
android:id="#+id/text_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="#dimen/text_size_medium"
tools:text="Label" />
<androidx.appcompat.widget.SwitchCompat
android:id="#+id/switch_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
Binding Adapters
#BindingAdapter("switchCheckedAttrChanged")
fun setListener(switchRow: SwitchRow, listener: InverseBindingListener) {
switchRow.switch_toggle.setOnCheckedChangeListener { _, _ ->
listener.onChange()
}
}
#BindingAdapter("switchChecked")
fun setSwitchChecked(switchRow: SwitchRow, value: Boolean) {
if (value != switchRow.switch_toggle.isChecked) {
switchRow.switch_toggle.isChecked = value
}
}
#InverseBindingAdapter(attribute = "switchChecked")
fun getSwitchChecked(switchRow: SwitchRow): Boolean {
return switchRow.switch_toggle.isChecked
}
Your InverseBindingAdapter should have the following annotation:
#InverseBindingAdapter(attribute = "switchChecked", event = "switchCheckedAttrChanged")
I'm trying to use 2-way databinding on a custom view that contains a SeekBar. The layout is rather simple, but I need to reuse it across the project, hence wrapping it into a custom view/component
<androidx.constraintlayout.widget.ConstraintLayout ... />
<TextView .../>
<TextView .../>
<SeekBar
android:id="#+id/ds_seekbar"
android:layout....
android:max="9"
android:min="0"
android:progress="0"
</androidx.constraintlayout.widget.ConstraintLayout>
The backing code looks like so (reduced)
CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
init {
LayoutInflater.from(context).inflate(R.layout.custom_view, this, true)
ds_description.setOnClickListener(this)
}
override fun onClick(view: View) {
//onClick implementation
}
}
I can do the binding in the ViewModel for the layout where this custom view is going to be used, with a BindingAdapter there with custom attribute (ex. app:seekbar), but the custom view would be used multiple times and I'd prefer to have the a lot of the logic that is required into the view and have a "lighter" handling in the ViewModel.
I read Android 2-Way DataBinding With Custom View and Custom Attr and a bunch of other articles which seem to be a little different but oon the same topic, however no matter how I wrote the getter and setters I always run into the kapt exception that it cannot find the getter/setter.
Either I'm not annotating properly the methods or they have wrong signatures.
Ideally I want to have something like:
CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener, SeekBar.OnProgressChangedListener {
... ds_seekbar.setOnProgressChangedListener(this)
And then in the main layout have the app:progress (or even better if someone can show how it's done android:progress) on the custom view for binding when passing my object.
Okay after more and more headscratching, here's what I've come with, that seems to work. Whether this is the proper way or how performant/reliable is - I'm not sure
#InverseBindingMethods(InverseBindingMethod(type = CustomView::class, attribute = "progress", event = "progressAttrChanged"))
CustomView #JvmOverloads constructor(...
private var progress = 0
private var mInverseBindingListener: InverseBindingListener? = null
cv_seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
progress = i + 1
if (mInverseBindingListener != null) {
mInverseBindingListener!!.onChange()
cv_indicator.text = progress.toString()
}
}...
})
fun getProgress(): Int {
return progress
}
fun setProgress(p: Int) {
if (progress != p) {
progress = p
}
}
fun setProgressAttrChanged(inverseBindingListener: InverseBindingListener?) {
if (inverseBindingListener != null) {
mInverseBindingListener = inverseBindingListener
}
}
Then the XML is
<com.xxx.CustomView
android:id="#+id/xxx"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:progress="#={viewModel.dataobject.value}"
....
/>