Project configuration
RecyclerView
, this trick called snapping list items to the center/left/right/top/bottom after scroll. From version 24.2.0, the design support library introduced two new classes (SnapHelper
and LinearSnapHelper
) that should be used to handle snapping in a RecyclerView
.Through this post, I will present the way to use 2 classes and building a snapping
RecyclerView
.Before start coding, make sure that you use
buildToolsVersion "24.0.2"
and support library version 24.2.0 (or later) for your project. Adding some necessary dependencies, your application-level build.gradle
can be like this:
build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
applicationId "info.devexchanges.snaprecyclerview"
minSdkVersion 14
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:recyclerview-v7:24.2.0'
compile 'com.android.support:cardview-v7:24.2.0'
compile 'com.android.support:appcompat-v7:24.2.0'
}
Creating center snapping RecyclerView
RecyclerView
):
activity_main.xml
Now, in Java code, if you use the default <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="info.devexchanges.snaprecyclerview.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
LinearSnapHelper
, you can only snap to the center.
The only code needed is:
SnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);
MainActivity.java
Running this activity, we'll have this result:
package info.devexchanges.snaprecyclerview;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SnapHelper;
import android.view.Gravity;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private ArrayList<Item> items;
private RecyclerView recyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
createApps();
/**
* Center snapping
*/
SnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);
SnapRecyclerAdapter adapter = new SnapRecyclerAdapter(this, items);
recyclerView.setAdapter(adapter);
}
private void createApps() {
items = new ArrayList<>();
items.add(new Item("Google+", R.drawable.google_plus));
items.add(new Item("Facebook", R.drawable.facebook));
items.add(new Item("LinkedIn", R.drawable.linkedin));
items.add(new Item("Youtube", R.drawable.youtube));
items.add(new Item("Instagram", R.drawable.instagram));
items.add(new Item("Skype", R.drawable.skype));
items.add(new Item("Twitter", R.drawable.twitter));
items.add(new Item("Wikipedia", R.drawable.wikipedia));
items.add(new Item("Whats app", R.drawable.what_apps));
items.add(new Item("Pokemon Go", R.drawable.pokemon_go));
}
}
More snapping options
calculateDistanceToFinalSnap()
and findSnapView()
methods of the LinearSnapHelper
to find the start view and calculate the distance needed to snap it to the correct position.
To make this easier to do, I created a GravitySnapHelper
class that supports snapping in 4 directions (start, top, end, bottom):
GravitySnapHelper.java
This GravitySnapHelper.java file was written by rubensousa on Github./*
* Copyright (C) 2016 The Android Open Source Project
* Copyright (C) 2016 RĂºben Sousa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific languag`e governing permissions and
* limitations under the License.
*/
package info.devexchanges.snaprecyclerview;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSnapHelper;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.View;
public class GravitySnapHelper extends LinearSnapHelper {
private OrientationHelper verticalHelper;
private OrientationHelper horizontalHelper;
private int gravity;
private boolean isSupportRtL;
@SuppressLint("RtlHardcoded")
public GravitySnapHelper(int gravity) {
this.gravity = gravity;
if (this.gravity == Gravity.LEFT) {
this.gravity = Gravity.START;
} else if (this.gravity == Gravity.RIGHT) {
this.gravity = Gravity.END;
}
}
@Override
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (recyclerView != null) {
isSupportRtL = recyclerView.getContext().getResources().getBoolean(R.bool.is_rtl);
}
super.attachToRecyclerView(recyclerView);
}
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
if (gravity == Gravity.START) {
out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
} else { // END
out[0] = distanceToEnd(targetView, getHorizontalHelper(layoutManager));
}
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
if (gravity == Gravity.TOP) {
out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager));
} else { // BOTTOM
out[1] = distanceToEnd(targetView, getVerticalHelper(layoutManager));
}
} else {
out[1] = 0;
}
return out;
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof LinearLayoutManager) {
switch (gravity) {
case Gravity.START:
return findStartView(layoutManager, getHorizontalHelper(layoutManager));
case Gravity.TOP:
return findStartView(layoutManager, getVerticalHelper(layoutManager));
case Gravity.END:
return findEndView(layoutManager, getHorizontalHelper(layoutManager));
case Gravity.BOTTOM:
return findEndView(layoutManager, getVerticalHelper(layoutManager));
}
}
return super.findSnapView(layoutManager);
}
private int distanceToStart(View targetView, OrientationHelper helper) {
if (isSupportRtL) {
return distanceToEnd(targetView, helper);
}
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
private int distanceToEnd(View targetView, OrientationHelper helper) {
if (isSupportRtL) {
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
}
private View findStartView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
if (layoutManager instanceof LinearLayoutManager) {
int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
if (firstChild == RecyclerView.NO_POSITION) {
return null;
}
View child = layoutManager.findViewByPosition(firstChild);
if (helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2
&& helper.getDecoratedEnd(child) > 0) {
return child;
} else {
if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
== layoutManager.getItemCount() - 1) {
return null;
} else {
return layoutManager.findViewByPosition(firstChild + 1);
}
}
}
return super.findSnapView(layoutManager);
}
private View findEndView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
if (layoutManager instanceof LinearLayoutManager) {
int lastChild = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
if (lastChild == RecyclerView.NO_POSITION) {
return null;
}
View child = layoutManager.findViewByPosition(lastChild);
if (helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2
<= helper.getTotalSpace()) {
return child;
} else {
if (((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition()
== 0) {
return null;
} else {
return layoutManager.findViewByPosition(lastChild - 1);
}
}
}
return super.findSnapView(layoutManager);
}
private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
if (verticalHelper == null) {
verticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return verticalHelper;
}
private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
if (horizontalHelper == null) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return horizontalHelper;
}
}
If you want the same behavior as the Google Play app, now you only need to change the previous code in
onCreate()
of MainActivity
to:
SnapHelper snapHelper = new GravitySnapHelper(Gravity.START);
snapHelper.attachToRecyclerView(recyclerView);
And we'll have this output:
To snapping on the right side, change the code above to:
SnapHelper snapHelper = new GravitySnapHelper(Gravity.END);
snapHelper.attachToRecyclerView(recyclerView);
And this is the result:
Moreover, combine with changing the
LinearLayoutManager
orientation, we'll have top/bottom snapping later:
/**
* Top snapping
*/
SnapHelper snapHelper = new GravitySnapHelper(Gravity.TOP);
snapHelper.attachToRecyclerView(recyclerView);
/**
* Bottom snapping
*/
//SnapHelper snapHelper = new GravitySnapHelper(Gravity.TOP);
//snapHelper.attachToRecyclerView(recyclerView);
// HORIZONTAL for Gravity START/END and VERTICAL for TOP/BOTTOM
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
recyclerView.setHasFixedSize(true);
Top snapping output:
Bottom snapping output:
Conclusions
RecyclerView
in 4 directions (start, top, end, bottom). Design support library is always updated by Google and now we are equipped with one more interesting feature to designing a good looking UI and well-behaving in UX. Hope this post can provide to you more information about RecyclerView
and material design. Finally, you can get full project code on Github.