Skip to content

Instantly share code, notes, and snippets.

@boybeak
Created April 21, 2020 06:00
Show Gist options
  • Save boybeak/1b387258bf11c7a3c50ae8380bf9136b to your computer and use it in GitHub Desktop.
Save boybeak/1b387258bf11c7a3c50ae8380bf9136b to your computer and use it in GitHub Desktop.
package com.github.boybeak.design.widget
import android.content.Context
import android.util.Log
import android.util.Size
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import com.github.boybeak.design.ext.isNulls
import kotlin.math.ceil
import kotlin.math.min
class GalleryLayoutManager(context: Context, /*private val orientation: Int, */private val gap: Int
) : RecyclerView.LayoutManager() {
companion object {
private val TAG = GalleryLayoutManager::class.java.simpleName
}
private val sizes = arrayOfNulls<Size>(4)
private var groupWidth: Int = 0
private var maxScrollX = 0
private var groupSizeWithWidth = 0
private val orientationHelper = OrientationHelper.createHorizontalHelper(this)
init {
/*if (orientation != RecyclerView.HORIZONTAL && orientation != RecyclerView.VERTICAL) {
throw IllegalArgumentException("Illegal orientation($orientation), " +
"must be RecyclerView.HORIZONTAL or RecyclerView.VERTICAL")
}*/
}
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
if (itemCount == 0) {
detachAndScrapAttachedViews(recycler)
return
}
if (childCount == 0 && state.isPreLayout) {
return
}
if (sizes.isNulls()) {
val h0 = height- 2 * gap
sizes[0] = Size(h0, h0)
val h1 = (height - 3 * gap) / 2
sizes[1] = Size(h0, h1)
val h2 = (h0 - gap) / 2
sizes[2] = Size(h2, h2)
sizes[3] = Size(h2, h2)
groupWidth = sizes[0]!!.width + sizes[1]!!.width + gap * 2
groupSizeWithWidth = getItemCountWithinWidth()
}
val groupCountTotally = itemCount / 4
val remainCount = itemCount % 4
val distance = groupCountTotally * groupWidth +
when (remainCount) {
0 -> {
gap
}
1 -> {
sizes[0]!!.width + gap * 2
}
else -> {
groupWidth + gap
}
}
maxScrollX = min(0, width - distance)
detachAndScrapAttachedViews(recycler)
for (p in 0 until itemCount) {
val m = p % sizes.size
val offsetX = getAbsoluteOffsetXForPosition(p) + scrollX
val offsetY = when(m) {
0,1 -> gap
2,3 -> gap * 2 + sizes[1]!!.height
else -> 0
}
val s = sizes[m]!!
val l = offsetX
val r = l + s.width
val t = offsetY
val b = t + s.height
if (r < 0) {
continue
}
if (l > width) {
break
}
val child = recycler.getViewForPosition(p)
child.layoutParams.apply {
width = s.width
height = s.height
}
measureChildWithMargins(child, s.width, s.height)
child.requestLayout()
addView(child)
layoutDecoratedWithMargins(child, l, t, r, b)
}
}
override fun canScrollHorizontally(): Boolean {
return true
}
private var scrollX = 0
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
): Int {
// <- positive
// -> negative
var predictSX = scrollX - dx
if (dx > 0 && predictSX < maxScrollX && scrollX >= maxScrollX) {
predictSX = maxScrollX
} else if (dx < 0 && predictSX > 0 && scrollX <= 0) {
predictSX = 0
}
val newDX = scrollX - predictSX
scrollX = predictSX
recycleViews(newDX, recycler)
fill(dx, recycler)
offsetChildrenHorizontal(-newDX)
return newDX
}
private fun fill(dx: Int, recycler: RecyclerView.Recycler) {
if (dx > 0) {
val lastVisibleView = getChildAt(childCount - 1) ?: return
val position = getPosition(lastVisibleView)
if (position >= itemCount - 1) {
return
}
val nextPosition = position + 1
val m = nextPosition % sizes.size
val s = sizes[m]!!
val nextView = recycler.getViewForPosition(nextPosition)
nextView.layoutParams.apply {
width = s.width
height = s.height
}
addView(nextView)
if (position == 9) {
Log.v(TAG, "dx=$dx fill addViewFor nextPosition=$nextPosition")
}
measureChild(nextView, s.width, s.height)
nextView.requestLayout()
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = when(m) {
0, 1, 3 -> lastVisibleView.right + gap
else -> lastVisibleView.left
}
val offsetY = when(m) {
0, 1 -> gap
else -> sizes[1]!!.height + 2 * gap
}
layoutDecorated(nextView, offsetX, offsetY, offsetX + viewWidth, offsetY + viewHeight)
} else if (dx < 0) {
val firstVisibleView = getChildAt(0) ?: return
val position = getPosition(firstVisibleView)
if (position <= 0) {
return
}
val prePosition = position - 1
val m = prePosition % sizes.size
val s = sizes[m]!!
val preView = recycler.getViewForPosition(prePosition)
preView.layoutParams.apply {
width = s.width
height = s.height
}
addView(preView)
measureChild(preView, s.width, s.height)
preView.requestLayout()
val viewWidth = getDecoratedMeasuredWidth(preView)
val viewHeight = getDecoratedMeasuredHeight(preView)
val offsetX = when(m) {
0 -> firstVisibleView.left - gap - s.width
1 -> firstVisibleView.left
2 -> firstVisibleView.left - gap - s.width
else -> firstVisibleView.left - gap - s.width
}
val offsetY = when(m) {
0, 1 -> gap
else -> sizes[1]!!.height + 2 * gap
}
layoutDecorated(preView, offsetX, offsetY, offsetX + viewWidth, offsetY + viewHeight)
}
}
private fun recycleViews(dx: Int, recycler: RecyclerView.Recycler) {
for (i in 0 until itemCount) {
val childView = getChildAt(i) ?: return
val position = getPosition(childView)
//左滑
if (dx > 0) {
//移除并回收 原点 左侧的子View
if (childView.right - dx < 0) {
removeAndRecycleViewAt(i, recycler)
}
} else { //右滑
//移除并回收 右侧即RecyclerView宽度之以外的子View
if (childView.left - dx > width) {
removeAndRecycleViewAt(i, recycler)
}
}
}
}
private fun getItemCountWithinWidth(): Int {
val groupCount = ceil(width * 1.0 / groupWidth).toInt()
return groupCount * sizes.size
}
private fun getAbsoluteOffsetXForPosition(position: Int): Int {
val g = position / sizes.size
val m = position % sizes.size
return groupWidth * g + when(m) {
0 -> gap
1 -> gap * 2 + sizes[0]!!.width
2 -> gap * 2 + sizes[0]!!.width
3 -> gap * 3 + sizes[0]!!.width + sizes[2]!!.width
else -> 0
}
}
}
<?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="match_parent"
style="@style/CardStyle"
app:cardCornerRadius="@dimen/radius_large">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#BBDEFB"
/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/number"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@drawable/fg_thumb"
android:textAppearance="@style/CaptionAppearanceBold.Inverse"/>
</androidx.cardview.widget.CardView>
package com.github.boybeak.donkey.adapter.holder
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.github.boybeak.adapter.AbsHolder
import com.github.boybeak.adapter.AnyAdapter
import com.github.boybeak.donkey.R
import com.github.boybeak.donkey.adapter.item.MediaImageItem
import com.github.boybeak.core.ext.thumb
import com.github.boybeak.donkey.databinding.ItemMediaImageBinding
import kotlinx.android.synthetic.main.item_media_image.view.*
import org.jetbrains.anko.dip
import java.io.File
class MediaImageHolder(v: View) : AbsHolder<MediaImageItem>(v) {
companion object {
private val TAG = MediaImageHolder::class.java.simpleName
}
private val binding = ItemMediaImageBinding.bind(v)
override fun onBind(item: MediaImageItem, position: Int, absAdapter: AnyAdapter) {
binding.image.thumb(File(item.source().content), true)
binding.number.text = position.toString()
Log.v(TAG, "onBind position=$position itemView=(${itemView.width}, ${itemView.height}) img=(${binding.image.width}, ${binding.image.height})")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment