I\'m try to achieve something similar to what the Camera app has for its modes:
I might probably not need to have a ViewPager, as seems tha
I gave this a try
5 items: https://drive.google.com/open?id=1RPyiY9UndXcrbfBDWLB-UklxjPKMiR8- 2 items: https://drive.google.com/open?id=1HkG8NShxQ3illFupK-urSPwsUhag74WS
First, apply an item decoration to center the first and last items:
class CenterDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
private var firstViewWidth = -1
private var lastViewWidth = -1
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition
val lm = parent.layoutManager as LinearLayoutManager
if (adapterPosition == 0) {
// Invalidate decorations when this view width has changed
if (view.width != firstViewWidth) {
view.doOnPreDraw { parent.invalidateItemDecorations() }
}
firstViewWidth = view.width
outRect.left = parent.width / 2 - view.width / 2
// If we have more items, use the spacing provided
if (lm.itemCount > 1) {
outRect.right = spacing / 2
} else {
// Otherwise, make sure this to fill the whole width with the decoration
outRect.right = outRect.left
}
} else if (adapterPosition == lm.itemCount - 1) {
// Invalidate decorations when this view width has changed
if (view.width != lastViewWidth) {
view.doOnPreDraw { parent.invalidateItemDecorations() }
}
lastViewWidth = view.width
outRect.right = parent.width / 2 - view.width / 2
outRect.left = spacing / 2
} else {
outRect.left = spacing / 2
outRect.right = spacing / 2
}
}
}
Now, LinearSnapHelper determines the center of a view and includes its decorations. You can create a custom one that excludes the decorations from the calculation to center the view only:
/**
* A LinearSnapHelper that ignores item decorations to determine a view's center
*/
class CenterSnapHelper : LinearSnapHelper() {
private var verticalHelper: OrientationHelper? = null
private var horizontalHelper: OrientationHelper? = null
private var scrolled = false
private var recyclerView: RecyclerView? = null
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolled) {
if (recyclerView.layoutManager != null) {
val view = findSnapView(recyclerView.layoutManager)
if (view != null) {
val out = calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view)
if (out != null) {
recyclerView.smoothScrollBy(out[0], out[1])
}
}
}
scrolled = false
} else {
scrolled = true
}
}
}
fun scrollTo(position: Int, smooth: Boolean) {
if (recyclerView?.layoutManager != null) {
val viewHolder = recyclerView!!.findViewHolderForAdapterPosition(position)
if (viewHolder != null) {
val distances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager!!, viewHolder.itemView)
if (smooth) {
recyclerView!!.smoothScrollBy(distances!![0], distances[1])
} else {
recyclerView!!.scrollBy(distances!![0], distances[1])
}
} else {
if (smooth) {
recyclerView!!.smoothScrollToPosition(position)
} else {
recyclerView!!.scrollToPosition(position)
}
}
}
}
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
if (layoutManager == null) {
return null
}
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager))
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
}
return null
}
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
this.recyclerView = recyclerView
recyclerView?.addOnScrollListener(scrollListener)
}
override fun calculateDistanceToFinalSnap(
layoutManager: RecyclerView.LayoutManager,
targetView: View
): IntArray? {
val out = IntArray(2)
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager))
} else {
out[0] = 0
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager))
} else {
out[1] = 0
}
return out
}
private fun findCenterView(
layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper
): View? {
val childCount = layoutManager.childCount
if (childCount == 0) {
return null
}
var closestChild: View? = null
val center: Int = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
var absClosest = Integer.MAX_VALUE
for (i in 0 until childCount) {
val child = layoutManager.getChildAt(i)
val childCenter = if (helper == horizontalHelper) {
(child!!.x + child.width / 2).toInt()
} else {
(child!!.y + child.height / 2).toInt()
}
val absDistance = Math.abs(childCenter - center)
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
}
}
return closestChild
}
private fun distanceToCenter(
layoutManager: RecyclerView.LayoutManager,
targetView: View,
helper: OrientationHelper
): Int {
val childCenter = if (helper == horizontalHelper) {
(targetView.x + targetView.width / 2).toInt()
} else {
(targetView.y + targetView.height / 2).toInt()
}
val containerCenter = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
return childCenter - containerCenter
}
private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (verticalHelper == null || verticalHelper!!.layoutManager !== layoutManager) {
verticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
}
return verticalHelper!!
}
private fun getHorizontalHelper(
layoutManager: RecyclerView.LayoutManager
): OrientationHelper {
if (horizontalHelper == null || horizontalHelper!!.layoutManager !== layoutManager) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return horizontalHelper!!
}
}
Usage:
class MainActivity : AppCompatActivity() {
private val snapHelper = CenterSnapHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.addItemDecoration(CenterDecoration(0))
snapHelper.attachToRecyclerView(recyclerView)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = object : RecyclerView.ViewHolder(
LayoutInflater.from(this@MainActivity).inflate(
R.layout.list_item,
parent,
false
)
) {}
holder.itemView.setOnClickListener {
if (holder.adapterPosition != RecyclerView.NO_POSITION) {
snapHelper.scrollTo(holder.adapterPosition, true)
}
}
return holder
}
override fun getItemCount(): Int = 20
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textView.text = "pos:$position"
}
}
}
}
Posting XML here in case someone wants to check this out:
MainActivity
<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="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="4dp"
android:layout_height="0dp"
android:background="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
list_item.xml
<TextView
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" android:id="@+id/textView"
android:layout_width="wrap_content" android:layout_height="@dimen/list_item_size"
android:background="?attr/selectableItemBackground" android:clickable="true"
android:focusable="true" android:gravity="center" android:maxLines="1" android:padding="8dp"
android:shadowColor="#222" android:shadowDx="1" android:shadowDy="1" android:textColor="#fff"
tools:targetApi="m" tools:text="@tools:sample/lorem"/>
EDIT: here's a sample of how to use this:
http://s000.tinyupload.com/?file_id=01184747175525079378
This is how i solved it. The problem on custom snaphelper and decorators is that they dont work with other libaries and custom Views. It also works with items with variable widths.
If you want to snap the items, just use the classic snaphelper on the recyclerview
public class CenterRecyclerView extends RecyclerView {
public CenterRecyclerView(@NonNull Context context) {
super(context);
}
public CenterRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CenterRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void updatePadding() {
post(() -> {
final DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
ViewHolder firstViewHolder = findViewHolderForAdapterPosition(0);
if (firstViewHolder != null) {
firstViewHolder.itemView.measure(WRAP_CONTENT, WRAP_CONTENT);
int viewWidth = firstViewHolder.itemView.getMeasuredWidth();
int padding;
if (screenHeight > screenWidth) {
//Portrait
padding = screenWidth / 2 - viewWidth / 2;
} else {
//Landscape
padding = screenHeight / 2 - viewWidth / 2;
}
setPadding(padding, 0, padding, 0);
} else {
Log.e("CenterRecyclerView", "Could not get first ViewHolder");
}
});
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
updatePadding();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updatePadding();
}
}