Skip to content

Instantly share code, notes, and snippets.

@kyawhtut-cu
Created September 21, 2023 03:32
Show Gist options
  • Save kyawhtut-cu/510dff986790a7df0f9be38ef3dd150e to your computer and use it in GitHub Desktop.
Save kyawhtut-cu/510dff986790a7df0f9be38ef3dd150e to your computer and use it in GitHub Desktop.
A lightweight tableView container helper based on recyclerView, Tiny and fast, Easy to use, No intrusion into business code. Java version https://github.com/crosswall/EasyTableScrollHelper
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_name"
android:layout_width="120dp"
android:layout_height="30dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:text="名称"
android:textColor="#333333"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/dividerOne"
android:layout_width="0.5dp"
android:layout_height="0dp"
android:background="#333333"
app:layout_constraintBottom_toBottomOf="@id/tv_name"
app:layout_constraintStart_toEndOf="@id/tv_name"
app:layout_constraintTop_toTopOf="@id/tv_name" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/dividerOne"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headerScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_value1"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="最新价"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value2"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="涨跌幅"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value3"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="换手率"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value4"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="总市值"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value3"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value5"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="年初至今"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value4"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value6"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="成交量"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value5"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:background="#333333"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider"
tools:listitem="@layout/item_table" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_name"
android:layout_width="120dp"
android:layout_height="60dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:text="神秘代码"
android:textColor="#333333"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider"
android:layout_width="0.5dp"
android:layout_height="0dp"
android:background="#333333"
app:layout_constraintBottom_toBottomOf="@id/tv_name"
app:layout_constraintStart_toEndOf="@id/tv_name"
app:layout_constraintTop_toTopOf="@id/tv_name" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="60dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/divider"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="table_scroll_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_value1"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="value 1"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value2"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="value 2"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value3"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="value 3"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value4"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="value 4"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value3"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value5"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="value 5"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value4"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_value6"
android:layout_width="100dp"
android:layout_height="0dp"
android:gravity="center"
android:text="value 6"
android:textColor="#999999"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_value5"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/view_other"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#B0FFEB3B"
android:gravity="center"
android:padding="20dp"
android:text="隐藏/显示区域"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.dindinn.dashboard.databinding.ActivityTableBinding
import com.dindinn.dashboard.databinding.ItemTableBinding
class TableActivity : AppCompatActivity() {
private lateinit var vb: ActivityTableBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vb = ActivityTableBinding.inflate(layoutInflater)
setContentView(vb.root)
vb.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
val adapter = Adapter()
adapter.setData(getDemoDataList())
TableScrollHelper.attachToRecyclerView(
vb.recyclerView,
adapter,
vb.headerScrollView
)
}
private fun getDemoDataList(): List<DemoData> {
return (0..160).map {
DemoData(
it % 3 == 0,
"神秘代码 $it"
)
}
}
private data class DemoData(
var expanded: Boolean,
var name: String,
)
private class Adapter : RecyclerView.Adapter<Adapter.ViewHolder>() {
private var itemList = mutableListOf<DemoData>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
return ViewHolder(
ItemTableBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(itemList[position])
}
override fun getItemCount(): Int {
return itemList.size
}
@SuppressLint("NotifyDataSetChanged")
fun setData(list: List<DemoData>) {
itemList.addAll(list)
notifyDataSetChanged()
}
class ViewHolder(
private val vb: ItemTableBinding
) : RecyclerView.ViewHolder(vb.root) {
fun bind(data: DemoData) {
vb.tvName.text = data.name
vb.viewOther.isVisible = data.expanded
vb.viewOther.setOnClickListener {
data.expanded = false
vb.viewOther.isVisible = false
}
}
}
}
}
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.widget.HorizontalScrollView
import android.widget.OverScroller
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import timber.log.Timber
import java.lang.reflect.Field
import kotlin.math.max
object TableScrollHelper {
private const val TAG = "TableScrollHelper"
fun <VB : RecyclerView.ViewHolder> attachToRecyclerView(
recyclerView: RecyclerView?,
adapter: RecyclerView.Adapter<VB>?,
headerScroll: ViewGroup?,
asyncScrollable: Boolean = true
) {
if (recyclerView == null) {
Timber.d("$TAG recyclerView not be null")
return
}
if (adapter == null) {
Timber.d("$TAG adapter not be null")
return
}
if (headerScroll == null) {
Timber.d("$TAG headerScroll not be null")
}
// add header scrollView
val parent = headerScroll?.parent as ViewGroup?
val scrollView = TableHorizonScrollView(recyclerView)
if (parent != null && headerScroll != null) {
val lp = headerScroll.layoutParams
parent.removeView(headerScroll)
scrollView.addView(headerScroll, lp)
parent.addView(
scrollView,
LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT
)
)
}
// init TableAdapterWrapper
val layoutManager = LinearLayoutManager(recyclerView.context)
layoutManager.orientation = LinearLayoutManager.VERTICAL
recyclerView.layoutManager = layoutManager
val wrapperAdapter = TableAdapterWrapper(recyclerView, adapter, scrollView, asyncScrollable)
recyclerView.adapter = wrapperAdapter
}
class TableHorizonScrollView : HorizontalScrollView {
companion object {
internal const val VIEW_TAG = "TableHorizonScrollView"
}
private var recyclerView: RecyclerView? = null
private var moveFlag: Boolean = false
private var scrollerField: Field? = null
private var overScrollX: Int = 0
private var callback: OnScrollXCallback? = null
constructor(recyclerView: RecyclerView) : this(recyclerView.context) {
this.recyclerView = recyclerView
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
initView()
}
fun setOnScrollXCallback(callback: OnScrollXCallback) {
this.callback = callback
}
fun abortScroll() {
try {
val scroller = scrollerField?.get(this) as OverScroller?
if (scroller != null && !scroller.isFinished) {
scroller.abortAnimation()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/*override fun overScrollBy(
deltaX: Int,
deltaY: Int,
scrollX: Int,
scrollY: Int,
scrollRangeX: Int,
scrollRangeY: Int,
maxOverScrollX: Int,
maxOverScrollY: Int,
isTouchEvent: Boolean
): Boolean {
Timber.i(
"$VIEW_TAG overScrollBy scrollX: %d , maxOverScrollX: %d , scrollRangeX: %d , isTouchEvent: %s , deltaX: %d",
scrollX,
maxOverScrollX,
scrollRangeX,
isTouchEvent,
deltaX
)
callback?.scrollX(scrollX)
return super.overScrollBy(
deltaX,
deltaY,
scrollX,
scrollY,
scrollRangeX,
scrollRangeY,
maxOverScrollX,
maxOverScrollY,
isTouchEvent
)
}*/
override fun onScrollChanged(scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY)
callback?.scrollX(scrollX)
val rv = recyclerView ?: return
val childCount = rv.childCount
(0..childCount).mapNotNull { rv.getChildAt(it) }.forEach { view ->
val scrollView = view.findViewWithTag<TableHorizonScrollView>(VIEW_TAG)
if (scrollView != null /*&& scrollView != this && moveFlag*/) {
scrollView.scrollTo(scrollX, 0)
}
}
Timber.i(
"$VIEW_TAG onScrollChanged scrollX: %d , oldScrollX: %d , currentScrollX: %d , overScrollX: %d",
scrollX,
oldScrollX,
scrollX,
overScrollX
)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
moveFlag = ev.actionMasked == MotionEvent.ACTION_DOWN || ev.actionMasked == MotionEvent.ACTION_MOVE
return super.dispatchTouchEvent(ev)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
val rv = recyclerView ?: return super.onTouchEvent(ev)
val childCount = rv.childCount
(0..childCount).mapNotNull { rv.getChildAt(it) }.forEach { view ->
val scrollView = view.findViewWithTag<TableHorizonScrollView>(VIEW_TAG)
scrollView?.abortScroll()
}
callback?.abort()
}
return super.onTouchEvent(ev)
}
override fun fling(velocityX: Int) {
// super.fling(velocityX)
val rv = recyclerView ?: return
val childCount = rv.childCount
(0..childCount).mapNotNull {
rv.getChildAt(it)
}.forEach { view ->
val scrollView = view.findViewWithTag<TableHorizonScrollView>(VIEW_TAG)
if (scrollView != null) {
val width = scrollView.width - scrollView.paddingRight - scrollView.paddingLeft
val right = scrollView.getChildAt(0).width
val scroller: OverScroller? = try {
scrollerField?.get(scrollView) as OverScroller
} catch (e: IllegalAccessException) {
e.printStackTrace()
null
}
scroller?.let {
it.fling(
scrollView.scrollX,
scrollView.scrollY,
velocityX,
0,
0,
max(0, right - width),
0,
0,
width / 2,
0
)
scrollView.postInvalidateOnAnimation()
}
}
}
}
private fun initView() {
tag = VIEW_TAG
overScrollMode = OVER_SCROLL_NEVER
try {
scrollerField = javaClass.superclass.getDeclaredField("mScroller")
scrollerField?.isAccessible = true
} catch (e: NoSuchFieldException) {
e.printStackTrace()
}
}
}
@Suppress("UNCHECKED_CAST")
class TableAdapterWrapper<VH : RecyclerView.ViewHolder>(
private val recyclerView: RecyclerView,
private val adapter: RecyclerView.Adapter<VH>,
private val tableHeaderView: TableHorizonScrollView,
private val asyncScrollable: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val SCROLL_ROOT_CONTAINER = "table_scroll_container"
}
private var currentScrollX: Int = 0
init {
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
@SuppressLint("NotifyDataSetChanged")
override fun onChanged() {
super.onChanged()
notifyDataSetChanged()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
notifyItemMoved(fromPosition, toPosition)
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
notifyItemRangeRemoved(positionStart, itemCount)
}
})
// recyclerView scroll callback
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!asyncScrollable) tableHeaderView.abortScroll()
// tableHeaderView.scrollTo(currentScrollX, 0)
val childCount = recyclerView.childCount
(0..childCount).mapNotNull { recyclerView.getChildAt(it) }.forEach { view ->
val scrollView: TableHorizonScrollView? = view.findViewWithTag(
TableHorizonScrollView.VIEW_TAG
)
if (scrollView != null) {
if (!asyncScrollable) scrollView.abortScroll()
// scrollView.scrollTo(currentScrollX, 0)
}
}
}
})
// header scrollView callback
tableHeaderView.setOnScrollXCallback(object : OnScrollXCallback {
override fun scrollX(scrollX: Int) {
}
override fun abort() {
if (!asyncScrollable) recyclerView.stopScroll()
}
})
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = adapter.onCreateViewHolder(parent, viewType)
val targetView = holder.itemView.findViewWithTag<View>(SCROLL_ROOT_CONTAINER)
if (targetView != null) {
val targetParent = targetView.parent as ViewGroup
val lp = targetView.layoutParams
val scrollView = TableHorizonScrollView(recyclerView)
targetParent.removeView(targetView)
scrollView.addView(targetView, lp)
scrollView.setOnScrollXCallback(object : OnScrollXCallback {
override fun scrollX(scrollX: Int) {
currentScrollX = scrollX
tableHeaderView.scrollTo(scrollX, 0)
}
override fun abort() {
if (!asyncScrollable) recyclerView.stopScroll()
tableHeaderView.abortScroll()
}
})
targetParent.addView(
scrollView,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
)
}
return holder
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
adapter.onBindViewHolder(holder as VH, position)
try {
holder.itemView.findViewWithTag<TableHorizonScrollView>(
TableHorizonScrollView.VIEW_TAG
)?.let {
syncScrollX(it, 0)
}
} catch (e: Exception) {
Timber.e("$TAG onBindViewHolder: %s", e.message)
}
}
override fun getItemCount(): Int {
return adapter.itemCount
}
private fun syncScrollX(scrollView: TableHorizonScrollView, delayMills: Long) {
scrollView.postDelayed({
scrollView.scrollTo(currentScrollX, 0)
scrollView.postInvalidate()
if (scrollView.scrollX != currentScrollX) syncScrollX(scrollView, 30)
Timber.w("$TAG currentScrollX: %d, scrollView.scrollX: %d", currentScrollX, scrollView.scrollX)
}, delayMills)
}
}
interface OnScrollXCallback {
fun scrollX(scrollX: Int)
fun abort()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment