Why are my custom view RecyclerView items "invisible" - android

I need to onDraw the items of a RecyclerView. Using an approach "discovered" at this SO link, I have gotten - um - partway there.
Note that I ultimately want to onDraw "over" the custom view. Meaning call super to let the default drawing occur, then paint over unused areas of the (view's) canvas.
Before starting down this "custom view to allow onDraw" road, I had what you see on the left below:
Afterward, I had all "invisible" views (middle image above). I say "invisible" because they were still there to be clicked. To help me visualize things (and a bit of onDraw proof of concept) I overrode onDraw in the custom PuzzleView view, simply calling canvas.drawRect to cover the entire canvas in Green, and now see the right image above.
I am not sure what I'm doing wrong here.
Also, if it occurs to you that - well, why don't I simply onDraw the whole thing - that's not practical for a variety of reasons.
So, here's my PuzzleAdapter as it is now:
class PuzzleAdapter(private val puzzles: List<Puzzle>) : RecyclerView.Adapter<PuzzleAdapter.PuzzleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PuzzleAdapter.PuzzleHolder {
//val v = LayoutInflater.from(parent.context).inflate(R.layout.item_puzzle, parent, false)
//not inflating, creating PuzzleView (which is inflating itself)
val v = PuzzleView(parent.context)
v.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
return PuzzleHolder(v)
}
override fun getItemCount() = puzzles.size
//unchanged between these two versions
override fun onBindViewHolder(h: PuzzleAdapter.PuzzleHolder, pos: Int) {
val p = puzzles[pos]
h.view.puzzleItem_text_ndx.text = "# " + p.descr()
h.view.puzzleItem_text_details.text = "Ndx: ${p.puzzleNdx}"
h.view.setOnClickListener {
Log.d("##", "PuzzleHolder.onClick (bunch#${p.parentBunch.bunchID}; puzzle#${p.puzzleNdx})")
val action = BunchFragmentDirections.navBunchToPuzzle(PuzzleParcel(p.parentBunch.bunchID, p.puzzleNdx))
it.findNavController().navigate(action)
}
}
inner class PuzzleHolder(v: View) : RecyclerView.ViewHolder(v) {
val view: PuzzleView
init {
view = v as PuzzleView
}
}
}
PuzzleAdapter before:
class PuzzleAdapter(private val puzzles: List<Puzzle>) : RecyclerView.Adapter<PuzzleAdapter.PuzzleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PuzzleAdapter.PuzzleHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_puzzle, parent, false)
return PuzzleHolder(v)
}
override fun getItemCount() = puzzles.size
//unchanged between these two versions
override fun onBindViewHolder(h: PuzzleAdapter.PuzzleHolder, pos: Int) {
val p = puzzles[pos]
h.view.puzzleItem_text_ndx.text = "# " + p.descr()
h.view.puzzleItem_text_details.text = "Ndx: ${p.puzzleNdx}"
h.view.setOnClickListener {
Log.d("##", "PuzzleHolder.onClick (bunch#${p.parentBunch.bunchID}; puzzle#${p.puzzleNdx})")
val action = BunchFragmentDirections.navBunchToPuzzle(PuzzleParcel(p.parentBunch.bunchID, p.puzzleNdx))
it.findNavController().navigate(action)
}
}
inner class PuzzleHolder(v: View) : RecyclerView.ViewHolder(v) {
var view: View = v
}
}
PuzzleView (the custom view I am using with the Adapter):
class PuzzleView : RelativeLayout {
constructor (context: Context) : super(context) { init(context, null, 0) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(context, attrs, 0) }
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { init(context, attrs, defStyle) }
private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) {
inflate(getContext(), R.layout.item_puzzle, this)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//this.layout(l, t, r, b)
}
val rect = Rect(0, 0, 0, 0)
val paint = Paint()
override fun onDraw(canvas: Canvas) {
//super.onDraw(canvas)
Log.d("##", "PuzzleView.onDraw()")
rect.right = width - 10
rect.bottom = height - 10
val bkgrColor = ContextCompat.getColor(App.context, R.color.Green)
paint.style = Paint.Style.FILL
paint.color = bkgrColor
canvas.drawRect(rect, paint)
}
}
A few thoughts on the above class/code:
override fun onLayout is required
I have other custom views that work fine (not in relation to a RecyclerView) with an empty onLayout
I tried (it's commented out in the above code) this.layout(l, t, r, b) but get a stack overflow exception
My only real thoughts here are (1) that there's something I'm supposed to be doing in this onLayout method, but I can't think of what; or (2) there's something wrong with the way that I'm inflating item_puzzle, but - again - I can't think of what. (I tried a few things on this, to no avail). I cannot think of anything else!
And here's all the other code I think could possibly be relevant:
From the Fragment containing the RecyclerView (it's what is shown in the above three images):
bunch_recycler.layoutManager = GridLayoutManager(this.context, 3)
bunch_recycler.adapter = PuzzleAdapter(bunch.puzzles)
Finally, the XML for the item itself, item_puzzle:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="#color/facadeLight"
app:cardElevation="0dp"
>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/shape_puzzle"
>
<TextView
android:id="#+id/puzzleItem_text_ndx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:fontFamily="#font/showg"
android:maxLines="1"
android:text="17"
android:textColor="#color/facadeDark"
android:textSize="32sp"
/>
<TextView
android:id="#+id/puzzleItem_text_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/puzzleItem_text_ndx"
android:layout_marginStart="8dp"
android:text="details"
android:textSize="12sp"
/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
Also, if "custom view to allow onDraw" isn't the correct (or best) way of accomplishing my goal here, please let me know that as well.
Using:
Windows 10
Android Studio 3.4
Kotlin 1.3.31
Gradle 3.4.0
and the following:
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha05'
implementation 'androidx.core:core-ktx:1.2.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0-beta01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-beta01'
implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'
Afterthought
I saw (in a 4 1/2-year-old youtube video overriding getView to assign subviews in a similar-seeming situation. There is no getView method to override in today's RecyclerView. The closest thing I see is getItemViewType(position: Int): Int which doesn't seem promising either (I read up on it a bit). Just thought I'd mention this in case it triggers a thought for you (where it didn't for me).

Related

Visible thin white slits between elements in RecyclerView

I'm creating a pixel art editor application with Android Studio using Kotlin. And - for this - I've decided to create a RecyclerView with a grid layout adapter which contains a custom View called a Pixel.
Whenever a Pixel is pressed, the colour turns black.
Here is the code:
Canvas Fragment:
package com.realtomjoney.pyxlmoose
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import com.realtomjoney.pyxlmoose.databinding.FragmentCanvasBinding
class CanvasFragment : Fragment() {
private var _binding: FragmentCanvasBinding? = null
private val binding get() = _binding!!
private lateinit var caller: CanvasFragmentListener
companion object {
fun newInstance(): CanvasFragment {
return CanvasFragment()
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is CanvasFragmentListener) {
caller = context
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCanvasBinding.inflate(inflater, container, false)
setUpRecyclerView()
return binding.root
}
private fun setUpRecyclerView() {
val context = activity as Context
binding.canvasRecyclerView.layoutManager = GridLayoutManager(context, 25)
val pixels = caller.initPixels()
binding.canvasRecyclerView.adapter = CanvasRecyclerAdapter(pixels, caller)
binding.canvasRecyclerView.suppressLayout(true)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Recycler Adapter:
class CanvasRecyclerAdapter(private val pixels: List<Pixel>,
private val caller: CanvasFragmentListener) :
RecyclerView.Adapter<RecyclerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder(LayoutInflater.from(parent.context), parent)
}
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val currentPixel = pixels[position]
holder.tileParent.addView(currentPixel)
holder.tileParent.setOnClickListener {
caller.onPixelTapped(currentPixel)
}
}
override fun getItemCount() = pixels.size
}
And ViewHolder:
class RecyclerViewHolder(inflater: LayoutInflater, parent: ViewGroup)
: RecyclerView.ViewHolder(inflater.inflate(R.layout.pixel_layout, parent, false)) {
val tileParent: SquareFrameLayout = itemView.findViewById(R.id.pixelParent)
}
Canvas Activity:
class CanvasActivity : AppCompatActivity(), CanvasFragmentListener {
private lateinit var binding: ActivityCanvasBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setBindings()
setUpFragment()
}
private fun setUpFragment() {
supportFragmentManager
.beginTransaction()
.add(R.id.fragmentHost, CanvasFragment.newInstance()).commit()
}
private fun setBindings() {
binding = ActivityCanvasBinding.inflate(layoutInflater)
setContentView(binding.root)
}
override fun initPixels(): List<Pixel> {
val list = mutableListOf<Pixel>()
for (i in 1..625) {
list.add(Pixel(this))
}
return list.toList();
}
override fun onPixelTapped(pixel: Pixel) {
pixel.setBackgroundColor(Color.BLACK)
}
}
Pixel:
class Pixel : View {
constructor(context: Context) : super(context)
constructor(context: Context, attributes: AttributeSet) : super(context, attributes)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
setMeasuredDimension(width, width)
}
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CanvasFragment"
android:id="#+id/fragmentHost">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/canvasRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Now, I understand this may not be the best approach for this, but that is besides the point.
The point is that when I run the app I get these visible thin white slits between each pixel:
Sometimes only one column has the issue:
In fact most of the time it's one column that does and another that doesn't:
Regardless of the grid size, I still see this visible annoyance.
Now, I am not sure if it's a rendering issue with my EMU - but it doesn't seem to be the case.
This is NOT an EMU issue, my friend installed the APK and sent a screenshot of his phone and it was still visible:
(Picture of friend's phone.)
This doesn't directly answer your question, but here's how you could write a single View class that displays pixel art. Canvas is not very intimidating if you are only drawing rectangles.
This class doesn't enforce itself to be square, but you can do that using your layout constraints. If it's a view in a ConstraintLayout, you could use app:layout_constraintDimensionRatio="w,1:1" for this, or whatever ratio matches your ratio of horizontal and vertical pixel counts (if there isn't padding).
Drawing does create Set copies, but you could change it to using a MutableSet if performance is a problem. Or an alternate strategy could be to use a 2D array of Booleans (or Int colors) so you don't even need a Pixel class.
If you were going to support color, you could add a color property to the Pixel class and then you would change the color of the paint for each pixel inside the loop in onDraw.
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
data class Pixel(val x: Int, val y: Int)
class PixelArtView(context: Context, attrs: AttributeSet) : View(context, attrs) {
var pixels: Set<Pixel> = emptySet()
set(value) {
if (field != value) invalidate()
field = value
}
var horizontalPixels: Int = 10
set(value) {
field = value
invalidate()
}
var verticalPixels: Int = 10
set(value) {
field = value
invalidate()
}
private val pixelWidth: Float
get() = (width - paddingLeft - paddingRight).toFloat() / horizontalPixels
private val pixelHeight: Float
get() = (height - paddingTop - paddingBottom).toFloat() / verticalPixels
var isInteractive = true
private var isErasing = false
private val paint = Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
}
init {
// So we can see something in the layout editor
if (isInEditMode) pixels = List(10) { Pixel(it, it) }.toSet()
}
override fun onDraw(canvas: Canvas) {
val pixelWidth = pixelWidth
val pixelHeight = pixelHeight
for (pixel in pixels) {
val left = paddingLeft + pixel.x * pixelWidth
val top = paddingTop + pixel.y * pixelHeight
canvas.drawRect(left, top, left + pixelWidth, top + pixelHeight, paint)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (isInteractive) {
val touchDown = event.actionMasked == MotionEvent.ACTION_DOWN
val touchMove = event.actionMasked == MotionEvent.ACTION_MOVE
if (touchDown || touchMove) {
val pixel = Pixel(
((event.x - paddingLeft) / pixelWidth).toInt().coerceIn(0, horizontalPixels - 1),
((event.y - paddingTop) / pixelHeight).toInt().coerceIn(0, verticalPixels - 1)
)
if (touchDown) {
isErasing = pixel in pixels
}
pixels = if (isErasing) pixels - pixel else pixels + pixel
return true
}
}
return super.dispatchTouchEvent(event)
}
}
As you guys had mentioned in the comments, the custom View class called Pixel contains the code which makes sure the width and height are the same:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
setMeasuredDimension(width, width)
}
I think as you guys pointed out, removing this code fixed the problem for me.
Since the onMeasure function is removed, the class Pixel is redundant, so I will switch it to a regular View class in the future.
Right now it looks like so, as you can see, no slits are visible:
If anyone is facing a similar niche problem like this, I would recommend removing the 'onMeasure()' with the setMeasuredDimensions function (if you have one similar to mine), the RecyclerView automatically makes sure the width and height are equal so it's redundant and is the root of many problems.
If anyone wants to contribute to the code, as I had seen some of you request, here is the link:
https://github.com/realtomjoney/PyxlMoose
I think I will be sticking with RecyclerView for now, as I disagree with the notion that Canvas is easier, it actually seems to be the opposite of the case from the code I've seen. But thanks anyways.

ViewHolder gets a null reference for a custom view when used in a RecycleView adapter

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)

AutoCompleteTextView dropdown not showing after device rotation

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)
}
}

Slight lag using button setOnTouchListener

I am working on a project that requires the exact moment a button was touched to be recorded. For this, I am using setOnTouchListener.
I have simplified the listener down to a simple print statement. Within Logcat, there is a very slight delay between when the button is touched, and when "TOUCHED" gets printed.
class MainActivity : AppCompatActivity() {
private lateinit var mp: MediaPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tapButton.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
println("ACTION_DOWN")
mp = MediaPlayer.create (this, R.raw.blip)
mp.start()
Toast.makeText(this#MainActivity, "ACTION_DOWN", Toast.LENGTH_SHORT).show()
background.setBackgroundColor(Color.parseColor("#ff0000"))
v.isPressed = true
} else if (event.action == MotionEvent.ACTION_UP) {
println("ACTION_UP")
background.setBackgroundColor(Color.parseColor("#ffffff"))
v.isPressed = false
}
true
}
}
}
XML (as basic as it can get!)
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:text="Button"
android:layout_width="188dp"
android:layout_height="288dp"
android:id="#+id/tapButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
How can I prevent this lag, so that I can get the precise moment the button was touched?
If it requires not using a button, and using a tappable view - that is fine too. It's imperative that this lag is removed. Additionally - if this lag is something that can't be addressed - perhaps I can offset the UI if I have the exact length (in milliseconds) that this lag occurs for. Open to ideas.
EDIT:
I believe I found the solution here; just don't know how to fix it.
How can I make a Button more responsive?
This thread states there's something called 'getTapTimeout' that intentionally puts a delay on a touch event to determine if it will be a tap or scroll. THE CULPRIT?!
How do I set this to 0??
Don't use logcat to tell. Logcat sends data to a separate process. It can be used for basic timings, but not for ms exact timings.
Every MotionEvent has a timestamp in it. You can get it with motionEvent.getEventTime(). Use that for your timing data.
You could create your own CustomView that extends View and overrides dispatchTouchEvent. You'll need to do something along the following lines,
interface CustomListener {
fun customOnTouch()
}
class CustomTouchView : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
private var customOnTouchListener:CustomListener? = null
public fun setCustomOnTouchListener(listener: CustomListener) {
this.customOnTouchListener = listener
}
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
customOnTouchListener?.customOnTouch()
return super.dispatchTouchEvent(event)
}
}

How to 2-way bind a Seekbar in custom view / component?

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}"
....
/>

Categories

Resources