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)
...
}
Related
I assume I'm missing something simple, but I can't seem to get joystick actions (X,Y,Z,RZ) from a game pad using View.OnGenericMotionListener in Android API 30. I followed an example (I thought) but joystick motions are undetected. If I replace OnGenericMotionListener and my onGenericMotion functions with OnTouchListener and onTouch I see exactly what I would expect from touches of the touchpad. In the code below I have reduced to a minimum to see the issue. With joystick = BasicMotion I do not get my debug log messages- I only get
I/ViewRootImpl#cc64931[MainActivity]: ViewPostIme key 1
If I change to joystick = BasicTouch I get my debug log.
What am I missing to get input from a game pad? main_activity and BasicMotion/BasicTouch classes below. Also note if I add the onGenericMotionEvent function to MainActivity, then I see joystick inputs there, so I know the game pad is working. Code is in Kotlin, but I'd be happy with a java solution.
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
// val joystick = BasicMotion(this)
val joystick = BasicTouch(this)
setContentView(joystick)
super.onCreate(savedInstanceState)
}
}
BasicMotion class:
class BasicMotion : SurfaceView, SurfaceHolder.Callback, View.OnGenericMotionListener {
constructor(context: Context?) : super(context) {
holder.addCallback(this)
setOnGenericMotionListener(this)
}
constructor(context: Context?, attributes: AttributeSet?) : super(context, attributes) {
holder.addCallback(this)
setOnGenericMotionListener(this)
}
constructor(context: Context?, attributes: AttributeSet?, style: Int) : super(
context, attributes, style
) {
holder.addCallback(this)
setOnGenericMotionListener(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {}
override fun onGenericMotion(v: View, event: MotionEvent): Boolean {
Log.d("Debug Motion", "detected")
return true
}
}
Equivalent BasicTouch class:
class BasicTouch : SurfaceView, SurfaceHolder.Callback, View.OnTouchListener {
constructor(context: Context?) : super(context) {
holder.addCallback(this)
setOnTouchListener(this)
}
constructor(context: Context?, attributes: AttributeSet?) : super(context, attributes) {
holder.addCallback(this)
setOnTouchListener(this)
}
constructor(context: Context?, attributes: AttributeSet?, style: Int) : super(
context, attributes, style
) {
holder.addCallback(this)
setOnTouchListener(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {}
override fun onTouch(v: View, event: MotionEvent): Boolean {
Log.d("Debug Touch", "detected")
return true
}
}
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)
}
}
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'm having a difficult time trying how to figure out how to create a callback in Kotlin using lambdas. I have a custom TextInputEditText and I want to implement a function that the activity can call when text changes.
Here is my custom EditText:
class EditTextEx : TextInputEditText, TextWatcher {
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 onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// Call the callback onTextAvailable with the EditText's text (s.toString)
}
override fun afterTextChanged(p0: Editable?) {
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
}
In my activity I want to have a callback that gets called when the onTextChanged event gets called. The callback in the custom control sends only the text back to the client. So in my activity, I want something like this:
editText.onTextAvailable(text -> do something )
It's actually quite easy to do, look:
inline fun EditText.onTextChanged(crossinline onTextChange: (String) -> Unit): TextWatcher {
val textWatcher = object: TextWatcher {
override fun afterTextChanged(editable: Editable) {
onTextChange(editable.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}
this.addTextChangeListener(textWatcher)
return textWatcher
}
Now you can call
editText.onTextChanged { text -> /* do something */ }
In addition to the solution by #EpicPandaForce, there are a couple other solutions. If you want to stick with using a class as you've shown in your example, then you can do this:
class EditTextEx : TextInputEditText, TextWatcher {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var mOnTextWatcherCallback: (m: String) -> Unit = {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (mOnTextWatcherCallback != null)
mOnTextWatcherCallback(s.toString())
}
override fun afterTextChanged(p0: Editable?) {
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
fun onTextChange(callback: (text: String) -> Unit) {
mOnTextWatcherCallback = callback
}
}
Then in your activity create a function:
fun onTextChange(text: String) {
// Do something with the text.
}
And then setup your callback as follows:
my_edittext.onTextChange(::onTextChange)
This solution allows you to re-use the same onTextChange function for other controls that want to use it as well.
If you prefer to use an interface to define the callback, do this:
class EditTextEx : TextInputEditText, TextWatcher {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private var mOnTextWatcherCallback: ITextWatcher? = null
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (mOnTextWatcherCallback != null)
mOnTextWatcherCallback!!.onTextChanged(s.toString())
}
override fun afterTextChanged(p0: Editable?) {
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
fun onTextChange(callback: ITextWatcher) {
mOnTextWatcherCallback = callback
}
}
Then in your activity, create the callback as follows:
val textChangeHandler = object: ITextWatcher {
override fun onTextChanged(text: String) {
var t = text
}
}
And then setup your callback for your edittext controls as follows:
my_edittext.onTextChange(textChangeHandler)
Try something like this:
fun getEditTextChange(editText: EditText, onTextChange: (String) -> Unit){
val tw = object: TextWatcher {
private var region = Locale.getDefault().language
override fun afterTextChanged(s: Editable?) {
onTextChange.invoke(s.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}
editText.addTextChangedListener(tw)
}
Hope it helps
I want to use a Spinner inside my Dialog. The Spinner appears and contains the data i want to show. But as soon as i select one of the items, memory usage shoots up and i end up getting an out of memory Exception. In logcat the first entry after selectin the item is D/AndroidRuntime: Shutting down VM. I have no stacktraces whatsoever. Only some info output from the gc and two warnings about throwing an OOM.
class SearchTypeSpinner(context: Context, attributeSet: AttributeSet? = null): LinearLayout(context, attributeSet), AdapterView.OnItemSelectedListener {
private val types: List<String>
private val mapping: MutableMap<String, Int>
private val spinner: Spinner
init {
types = context.resources.getStringArray(R.array.filter_types).asList()
spinner = Spinner(context)
spinner.adapter = Adapter()
spinner.onItemSelectedListener = this
spinner.layoutParams = ViewGroup.LayoutParams(context, attributeSet)
addView(spinner)
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {}
override fun onNothingSelected(parent: AdapterView<*>?) {}
private inner class Adapter: ArrayAdapter<String>(context, android.R.layout.simple_spinner_item) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = TextView(context)
view.layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
view.text = getTypeNameAtPosition(position)
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
return getView(position, convertView, parent)
}
override fun getCount(): Int {
return types.size
}
}
}
Please let me know if i can provide additional information and thanks in advance.