I am new in android with kotlin.
I have recylerview with unlimit amount of items. But i want to show only 6 items per view. I used constraint layout in items to bind data. I used setOnTouchListener and used drag functionality. If my drag intensity is short and direction is on left side so i passed the 2 and if its in right direction i passed -2. If my drag intensity is long and direction is on left side so i passed 6 and if in right direction i passed -6 in smoothScrollBy function by pixel. I am using smoothScrollBy, the items are scrolling fine but if i could drag quickly the first and last items are cut in the edge side. Then i am using normal scrollby its working fine. Can you please suggest me some solution as to why this is happening?
Adapter file
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView: View =
LayoutInflater.from(parent.context)
.inflate(R.layout.calender_item_layout, parent, false)
itemView.layoutParams.width = Resources.getSystem().displayMetrics.widthPixels / 6
return ViewHolder(itemView)
}
CustomeRecyclerView
class CustomRecyclerView(context: Context, attrs: AttributeSet?) :
RecyclerView(context, attrs) {
init{
layoutManager = LinearLayoutManager(context).apply {
orientation = LinearLayoutManager.HORIZONTAL
stackFromEnd = true
}
setOnTouchListener(object : OnSwipeTouchListener() {
// pass 2 or 6 based on drag intensity
// 2 or -2 for short drag 6 and -6 for long drag
post { scrollItem(value) }
}
}
fun scrollItem(value: Int) {
smoothScrollBy(inpixel(value), 0)
}
private fun inpixel(offset: Int): Int {
val dim = Resources.getSystem().displayMetrics
return (dim.widthPixels / 6) * offset
}
}
thanks
Related
I am working on an idea, which is make a RecyclerView auto scrolling but allow user to click item without stop scrolling.
First, I create a custom LayoutManager to disable manual scroll, also change the speed of scroll to a certain position
class CustomLayoutManager(context: Context, countOfColumns: Int) :
GridLayoutManager(context, countOfColumns) {
// Custom smooth scroller
private val smoothScroller = object : LinearSmoothScroller(context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float =
500f / displayMetrics.densityDpi
}
// Disable manual scroll
override fun canScrollVertically(): Boolean = false
// Using custom smooth scroller to control the duration of smooth scroll to a certain position
override fun smoothScrollToPosition(
recyclerView: RecyclerView,
state: RecyclerView.State?,
position: Int
) {
smoothScroller.targetPosition = position
startSmoothScroll(smoothScroller)
}
}
Then I do the initial work for the RecyclerView and start smooth scroll after 1 sec
viewBinding.list.apply {
// initial recycler view
setHasFixedSize(true)
customLayoutManager = CustomLayoutManager(context = context, countOfColumns = 2)
layoutManager = customLayoutManager
// data list
val dataList = mutableListOf<TestModel>()
repeat(times = 100) { dataList.add(TestModel(position = it, clicked = false)) }
// adapter
testAdapter =
TestAdapter(clickListener = { testAdapter.changeVhColorByPosition(position = it) })
adapter = testAdapter
testAdapter.submitList(dataList)
// automatically scroll after 1 sec
postDelayed({ smoothScrollToPosition(dataList.lastIndex) }, 1000)
}
Everything goes as my expected until I found that the auto scrolling stopped when I clicked on any item on the RecycelerView, the function when clickListener triggered just change background color of the view holder in TestAdapter
fun changeVhColor(position: Int) {
position
.takeIf { it in 0..itemCount }
?.also { getItem(it).clicked = true }
?.also { notifyItemChanged(it) }
}
here is the screen recording screen recording
issues I encounter
auto scrolling stopped when I tap any item on the ReycelerView
first tap make scrolling stopped, second tap trigger clickListener, but I expect to trigger clickListener by one tap
Can anybody to tell me how to resolve this? Thanks in advance.
There is a lot going on here. You should suspect the touch handling of the RecyclerView and, maybe, the call to notifyItemChanged(it), but I believe that the RecyclerView is behaving correctly. You can look into overriding the touch code in the RecyclerView to make it do what you want - assuming you can get to it and override it.
An alternative would be to overlay the RecyclerView with another view that is transparent and capture all touches on the transparent view. You can then write code for the transparent view that interacts with the RecyclerView in the way that meets your objectives. This will also be tricky and you will have to make changes to the RecyclerView as it is constantly layout out views as scrolling occurs. Since you have your own layout manager, this might be easier if you queue changes to occur pre-layout as scrolling occurs.
After tried several ways, found that the key of keep recycler view scrolling automatically is override onInterceptTouchEvent
Example
class MyRecyclerView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : RecyclerView(context, attrs, defStyle) {
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean = false
}
that will make the custom RecyclerView ignore all touch event
I am attempting to write a recyclerview which has some of the Viewholders inside it as stacked ontop of one another. The idea is that you can drag the topmost view above the stacked list and have drop it above where it becomes separate.
I managed to get this working using a Recyclerview with a custom RecyclerView.ItemDecoration. However, after I drop the item, i have the adapter call notifyDataSetChange to update the background code. This causes the the next item in the stack to appear to be the wrong one (though this does change sometimes if you touch the item and start scrolling, then it displays the correct one).
The custom RecyclerView.ItemDecoration class:
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
val itemPosition = parent.getChildAdapterPosition(view)
val adapter = parent.adapter
if (adapter is BaseRecVAdapter)
{
val item = adapter.getDataModel(itemPosition)
if (item is DragDropModel && item.mStackedPos != PMConsts.negNum)
{
if (item.mStackedPos != 0)
{
val context = view.context
val top = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 148f, context.resources.displayMetrics).toInt()
outRect.set(0, -top, 0, 0)
return
}
}
}
super.getItemOffsets(outRect, view, parent, state)
}
The drag interface I made for the Adapter and the ItemTouchHelper.Callback can be found below:
interface ItemTouchHelperListener
{
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
fun onClearView(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?)
}
The onItem move code is as follows:
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
{
var newToPosition = toPosition
if (toPosition <= mDragUpLimit)
{//Prevent items from being dragged above maximum movement.
newToPosition = mDragUpLimit + 1
}
else if (toPosition >= mDragDownLimit)
{//Cannot drag below stacked List...
newToPosition = mDragDownLimit - 1
}
if (fromPosition < newToPosition)
{
for (i in fromPosition until newToPosition)
{
swap(mDataList, i, i + 1)
}
}
else
{
for (i in fromPosition downTo newToPosition + 1)
{
swap(mDataList, i, i - 1)
}
}
notifyItemMoved(fromPosition, newToPosition)
return true
}
I have a simple viewholder which is an invisible bar which i mark as the position you need to drag above in order to make a valid change to the list order.
I have the code call notifyDataSetChanged after the onClearView() method is called as I need to update the background features so that the next item in the stack is draggable and the background data feeding into the adapter is also updated. It seems the simplest way to keep the data updating smoothly, but I wonder if it is causing my problems
If someone would be able to give me a hand with this, I would be most grateful. I am tearing my hair out somewhat. I thought I had a good system setup but it was not quite working. I hope that this is enough information to get some help with this issue.
Thank you in advance
I have a RecyclerView that contains TextViews. The number of TextViews can vary and the size of them vary as well and can be dynamically changed.
When the user scrolls to a certain position within the list and exits app, I want to be able to return to that exact position in the next session.
To do this, I need to know how many pixels have scrolled past from where the current TextView in view started and where the current position of the scroll is. For example, if the user has the 3rd TextView in view and scrolls 100 pixels down from where that TextView started, I will be able to return to this spot with scrollToPositionWithOffset(2, 100). If the TextView changes size (due to font changes), I can also return to the same spot by calculating the percentage of offset using the TextView's height.
Problem is, I cannot get the offset value in any accurate manor.
I know I can keep a running calculation on the Y value scrolled using get scroll Y of RecyclerView or Webview, but this does not give me where the TextView actually started. I can listen to when the user scrolled past the start of any TextView and record the Y position there but this will be inaccurate on fast scrolling.
Is there a better way?
Don't use position in pixels, use the index of the view. Using layout manager's findFirstVisibleItemPosition or findFirstCompletelyVisibleItemPosition.
That's a very popular question, although it may be intuitive to think and search for pixels not index.
Get visible items in RecyclerView
Find if the first visible item in the recycler view is the first item of the list or not
how to get current visible item in Recycler View
A good reason to not trust pixels is that it's not useful on some situations where index is, like rotating the screen, resizeing/splitting the app size to fit other apps side by side, foldable phones, and changing text / screen resolution.
I solved this by converting to a ListView:
lateinit var adapterRead: AdapterRead // Custom Adapter
lateinit var itemListView: ListView
/*=======================================================================================================*/
// OnViewCreated
itemListView = view.findViewById(R.id.read_listview)
setListView(itemListView)
// Upon entering this Fragment, will automatically scroll to saved position:
itemListView.afterMeasured {
scrollToPosition(itemListView, getPosition(), getOffset())
}
itemListView.setOnScrollListener(object : AbsListView.OnScrollListener {
private var currentFirstVisibleItem = 0
var offset = 0
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// When scrolling stops, will save the current position and offset:
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
offset = if(itemListView.getChildAt(0) == null) 0 else itemListView.getChildAt(0).top - itemListView.paddingTop
saveReadPosition(getReadPosition(itemListView), offset)
}
}
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
currentFirstVisibleItem = firstVisibleItem
}
})
/*=======================================================================================================*/
// Thanks to https://antonioleiva.com/kotlin-ongloballayoutlistener/ for this:
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
/*=======================================================================================================*/
fun setListView(lv: ListView) {
adapterRead = AdapterRead(list, context!!)
lv.apply {this.adapter = adapterRead}
}
/*=======================================================================================================*/
fun scrollToPosition(lv: ListView, position: Int, offset: Int) {
lv.post { lv.setSelectionFromTop(position, offset) }
}
/*=======================================================================================================*/
fun saveReadPosition(position: Int, offset: Int) {
// Persist your data to database here
}
/*=======================================================================================================*/
fun getPosition() {
// Get your saved position here
}
/*=======================================================================================================*/
fun getOffse() {
// Get your saved offset here
}
I have a custom view in which you can draw with your fingers, I fill out a recyclerview with this view, made an adapter, and for example a list with size = 10 is obtained, where each item is a custom view, and if the user draws for example in item with position 5 then when scrolling the same picture appears in another item where he did not draw.
here is my adapter:
class CustomViewListAdapter : RecyclerView.Adapter<CustomViewListAdapter.ViewHolder>() {
private var listSchedule = ArrayList<Int>()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var calendarView: CustomView= itemView.calendar!!
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.schedule_item,
parent,
false
)
)
}
override fun getItemCount(): Int = listSchedule.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.calendarView.setValue(listSchedule[position] )
}
fun setSchedules(listSchedule: ArrayList<Int>) {
this.listSchedule = listSchedule
notifyDataSetChanged()
}
}
Are you aware of how RecyclerView works? Lets assume you have 1000 items in your list. A simple approach would be to create 1000 views from your schedule_item layout bind them and show them in e.g. a scrollview. However, doing that would cost a lot of time and memory. So the clever Android developers came up with the following idea:
Since of the 1000 items only e.g. 10 are visible at the same time, lets just create 10 views and only change the content of the views depending on the actual item they show. So the 10 views are reused or recycled.
For this to work, the onBindViewHolder implementation must make sure that it updates the provided viewholder in a way that it shows the content of the item at the given position.
Now in your code, all you do in onBindViewHolder is, to set a single integer. There is no sign of setting any custom drawing etc. So I assume the drawing is just stored inside the CustomView. And since there are only those 10 or so CustomViews (as explained above), when they are reused to show a different item, they contain the original drawings because you didn't change that in onBindViewHolder.
I feel like I am being silly,
I want to use MotionLayout on an ViewHolder within my RecyclerView to animate between two states (the current playing song is expanded, while the last playing song is shrunk)
However it seems that the recyclerview is too good, it simply changes the contents without changing the views, i.e. when the current playing song changes, the view is already in the End Transition state, so my transition does nothing.
Same my previously expanded item is rebound into the closed state so my animation does nothing.
So Okay i thought lets check the state of the transition and set the progress, but this leads to the transition not running if i set the progress the line before. I have tried, adding in some delays but no real improvement,
I feel like maybe I am over engineering this, or I am missing something fundamental about how to reset motionlayout animations.. Any help will be much appreciated.
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
if (songs.isNotEmpty() && position < songs.size) {
val current = songs[position]
holder.songName?.text = current.song
holder.albumArt?.setImageResource(current.albumArtId)
holder.artistName?.text = current.artist
var ml = holder.motionLayout
if (current.currentPlaying){
//The view is recycled so its already in the end state... so set it to the start state and animate
if (ml?.progress!! > 0f) {
ml?.progress = 0f //<- This resets the animation state
}
ml?.transitionToEnd() <- but at this point the animation does not work if i have manually set the progress the line above
}else{
if (current.previoussong){
//The view that was last expended is not in the end state, so set it then transation to start
if (ml?.progress!! < 1f) {
ml?.progress = 1f
}
ml?.transitionToStart()
}
}
}
}
Okay incase anyone has the same issue, i found "an" answer to my dilemma.
Insteead of setting the progress, i explicitly set the transition then asked it to transition to end, this worked for expand.
And to get shrink working i had to create a different initial layout with an inverted motionscene, then transition to the end, and set the "previoussong" as a different viewType in my creatViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
//Use the int to switch?
val itemView : View = when (viewType) {
TYPEPLAYING -> inflater.inflate(R.layout.list_motion_layout, parent, false)
TYPEUPCOMING -> inflater.inflate(R.layout.list_motion_layout, parent, false)
TYPEPREVIOUS -> inflater.inflate(R.layout.list_motion_layout_shrink, parent, false)
else
-> inflater.inflate(R.layout.footer, parent, false)
}
...
}
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
if (songs.isNotEmpty() && position < songs.size) {
val current = songs[position]
holder.songName?.text = current.song
holder.albumArt?.setImageResource(current.albumArtId)
holder.artistName?.text = current.artist
var ml = holder.motionLayout
if (current.currentPlaying){
ml?.setTransition(list_motion_layout_start, currentplaying_song)
ml?.transitionToEnd()
}
if (current.previouslyPlaying) {
ml?.setTransition(currentplaying_song, list_motion_layout_start) // Not sure if this is actually required
ml?.transitionToEnd()
}
}
}
All in all i have a working expand/shrink list adapter using motionview it looks quite nice, but there is probably nicer ways to do this out there ,,, sorry cant share any pics.