I wonder how I can achieve such behaviour of the RecyclerView, that I could scroll the list only when I click and drag only specific view within the ViewHolder?
I've disabled horizontal scroll by creating my own LinearLayoutManager:
class MyOwnLayoutManager : LinearLayoutManager {
constructor(context: Context?) : super(context)
constructor(context: Context?, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
private var isScrollEnabled = false
fun setScrollEnabled(isEnabled: Boolean) {
isScrollEnabled = isEnabled
}
override fun canScrollVertically(): Boolean {
return isScrollEnabled && super.canScrollVertically()
}
override fun canScrollHorizontally(): Boolean {
return isScrollEnabled && super.canScrollHorizontally()
}
}
Then, I try to change the isScrollEnabled by setting the touch listener to the item's header:
item_header.setOnTouchListener { v, event ->
val isScrolling = event.action == ACTION_MOVE
onHeaderIsDragging.invoke(isScrolling)
false
}
Callback in the fragment that changes adapters' layout manager var:
private val onHeaderIsDragging: ((Boolean) -> Unit) = {
recyclerViewLayoutManager.setScrollEnabled(it)
}
By this implementation, I get MOTION_CANCEL after a couple of MOTION_MOVE events in the onTouchListener and RecyclerView is not scrollable after MOTION_MOVE events.
Related
I am implementing a rotary knob based on this library.
This View comes with a onStateChanged listener that tells me the current position of the knob.
val knob = findViewById<View>(R.id.knob) as Knob
knob.setOnStateChanged(object: Knob.OnStateChanged{
override fun onState(state: Int) {
// do stuff
}
})
In addition to that, I want to know when the user is no longer pressing/holding the knob (similar to a button release). I tried to achieve this with a onTouch listener.
knob.setOnTouchListener(object: View.OnTouchListener{
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
// do stuff
}
})
Problem: When I add a second onTouch listener, it is no longer possible to hold and rotate the view for some reason. I do not know whether this is a problem of this particular library or Android in general.
Any suggestions on how to implement the wanted features?
A view can only have a single OnTouchListener. Unfortunately, the author of that library implemented some of its functionality using an OnTouchListener, which prevents users from using an OnTouchListener.
An alternative for you would be to subclass Knob and override onMotionEvent, like this:
class MyKnob: Knob {
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)
override fun onTouchEvent(event: MotionEvent): Boolean {
val result = super.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP) {
// User released knob
}
return result
}
}
Then you'd need to use this class in your layout instead of the original Knob class.
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 have recyclerView with and item as seen below:
Now I want to be able to click anywhere on the item and editText should come into focus.
I can do that by setting onTouchListener on my view like this:
row_item.setOnTouchListener{ _, _ ->
editText.requestFocus()
view.background = Color.GREEN.toDrawable()
true
}
I also want to run some additional code whenever the item is clicked. Here I'm putting background color change for the sake of the example.
The problem is that whenever I click editText itself it is getting focused, but row_item touchListener is ignored, and the background doesn't change its color.
From my research, I've found that I should somehow intercept touch event. I thought I can do that by returning true in row_item.setOnTouchListener, but it doesn't work as you can see.
How can I intercept such touch event?
You should create a custom container class and then override onInterceptTouchEvent method and do your stuff there, then using this custom container class as the root of the item. Look at the following code:
class CustomFrameLayout : FrameLayout {
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 mOnInterceptTouchEventListener: OnTouchListener? = null
fun setOnInterceptTouchEventListener(onInterceptTouchEventListener: OnTouchListener) {
this.mOnInterceptTouchEventListener = onInterceptTouchEventListener
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if(mOnInterceptTouchEventListener != null && mOnInterceptTouchEventListener?.onTouch(this, ev) == true)
return true;
return super.onInterceptTouchEvent(ev)
}
}
And then adding this listener to your row_item
row_item.setOnInterceptTouchEventListener(object: View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
editText.requestFocus()
v?.background = Color.GREEN.toDrawable()
return false
}
})
it may not be the best answer but it works.
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'm extending LinearLayout and overriding onInterceptTouchEvent and onTouchEvent because I want to receive all touch event on all children views. I'm only interested in receiving the ACTION_DOWN and ACTION_UP MotionEvents. I still want the children view to receive all the events.
The problem I found is that if
Return false from onInterceptTouchEvent: all subsequent MotionEvents are sent to the children views and I will not receive ACTION_UP.
Return true from onInterceptTouchEvent: all subsequent MotionEvents are sent to my onTouchEvent view and the children views will not receive any.
class LinearLayout : android.widget.LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attributeSet, defStyleAttr, defStyleRes)
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
Log.d("LinearLayout", "onInterceptTouchEvent >> $event")
return when (event.action) {
MotionEvent.ACTION_DOWN -> true // or false
MotionEvent.ACTION_UP -> true
else -> false
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
Log.d("LinearLayout", "onTouchEvent >> $event")
return true
}
}
Is there any way to intercept all MotionEvents and send them all to the children views? If not, is there any other way to intercept all touch events over a ViewGroup?