So I recently had to implement the following gif in Android.
I needed to update the Background of the Activity when the RecyclerView scrolls and a select item is at the center. So let's see how to implement this.
Step 1: Create an interface class:
This is to update the UI when a RecyclerView is focused at the center(i.e. when a single item from the list is at the center of the screen).
public interface RecyclerSnapItemListener {
void onItemSnap(int position);
}
Step 2: We make use of the SnapHelper
class of RecyclerView
.
SnapHelper
is a helper class that helps in snapping any child view of the RecyclerView. We implement our own version of SnapHelper class. Android offers two variants for you: LinearSnapHelper
and PagerSnapHelper
.
LinearSnapHelper
snaps to the item that is closest to the middle of the RecyclerView.PagerSnapHelper
offers similar behavior to a ViewPager but requires that your item views have their layout parameters set to MATCH_PARENT.
I strongly recommend going through this article to understand more aboutSnapHelper
.
Now we are going to create a custom SnapHelper
class extending LinearSnapHelper
. We are going to pass the interface we created in the previous step to the custom SnapHelper class.
public class PagerSnapHelper extends LinearSnapHelper {
private RecyclerSnapItemListener recyclerSnapItemListener;
private OrientationHelper mVerticalHelper, mHorizontalHelper;
public PagerSnapHelper(RecyclerSnapItemListener recyclerSnapItemListener) {
this.recyclerSnapItemListener = recyclerSnapItemListener;
}
@Override
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
super.attachToRecyclerView(recyclerView);
}
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof LinearLayoutManager) {
if (layoutManager.canScrollHorizontally()) {
return getStartView(layoutManager, getHorizontalHelper(layoutManager));
} else {
return getStartView(layoutManager, getVerticalHelper(layoutManager));
}
}
return super.findSnapView(layoutManager);
}
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
View centerView = findSnapView(layoutManager);
if (centerView == null)
return RecyclerView.NO_POSITION;
int position = layoutManager.getPosition(centerView);
int targetPosition = -1;
if (layoutManager.canScrollHorizontally()) {
if (velocityX < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
if (layoutManager.canScrollVertically()) {
if (velocityY < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
final int firstItem = 0;
final int lastItem = layoutManager.getItemCount() - 1;
targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
/*
* You can see here that we find the targetPosition and pass that to the interface.
* Using this targetPositon, we would be able to find out which item is at the center.
*/
if(targetPosition >= 0) recyclerSnapItemListener.onItemSnap(targetPosition);
return targetPosition;
}
private int distanceToStart(View targetView, OrientationHelper helper) {
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
private View getStartView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
if (layoutManager instanceof LinearLayoutManager) {
int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
boolean isLastItem = ((LinearLayoutManager) layoutManager)
.findLastCompletelyVisibleItemPosition()
== layoutManager.getItemCount() - 1;
if (firstChild == RecyclerView.NO_POSITION || isLastItem) {
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 OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
}
Step 3: Attach the RecyclerView in our UI to the custom SnapHelper
class.
SnapHelper startSnapHelper = new PagerSnapHelper(new RecyclerSnapItemListener() {
@Override
public void onItemSnap(int position) {
MovieEntity movie = moviesListAdapter.getItem(position);
/* here we get the movie item that is currently at the center.
* now we can just update the background of the screen simply by calling
* layout.setBackgroundImage(movie.getImage());
* OR
* we can use BackgroundSwitcherView to change the background image.
* backgroundSwitcherView.updateCurrentBackground(movie.getPosterPath());
*/
}
});
/*
* Here we attach the recyclerView to the SnapHelper
*/
startSnapHelper.attachToRecyclerView(binding.moviesList);
Step 4: Add animation to the background layout when it is in focus:
You must have noticed in the gif image that the background movies everytime a new image is set. This is done with a help of a custom class: BackgroundSwitcherView
which extends ImageSwitcher
. ImageSwitcher
is a ViewSwitcher
that switches between two ImageViews when a new image is set on it.
public class BackgroundSwitcherView extends ImageSwitcher {
private final int[] NORMAL_ORDER = new int[]{0, 1};
private int bgImageGap;
private int bgImageWidth;
private Animation bgImageInLeftAnimation;
private Animation bgImageOutLeftAnimation;
private Animation bgImageInRightAnimation;
private Animation bgImageOutRightAnimation;
private int movementDuration = 500;
private int widthBackgroundImageGapPercent = 12;
private AnimationDirection currentAnimationDirection;
public BackgroundSwitcherView(Context context, AttributeSet attrs) {
super(context, attrs);
inflateAndInit(context);
}
public BackgroundSwitcherView(Context context) {
super(context);
inflateAndInit(context);
}
private void inflateAndInit(final Context context) {
setChildrenDrawingOrderEnabled(true);
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
bgImageGap = (displayMetrics.widthPixels / 100) * widthBackgroundImageGapPercent;
bgImageWidth = displayMetrics.widthPixels + bgImageGap * 2;
this.setFactory(() -> {
ImageView myView = new ImageView(context);
myView.setScaleType(ImageView.ScaleType.CENTER_CROP);
myView.setLayoutParams(new LayoutParams(bgImageWidth, LayoutParams.MATCH_PARENT));
myView.setTranslationX(-bgImageGap);
return myView;
});
bgImageInLeftAnimation = createBgImageInAnimation(bgImageGap, 0, movementDuration);
bgImageOutLeftAnimation = createBgImageOutAnimation(0, -bgImageGap, movementDuration);
bgImageInRightAnimation = createBgImageInAnimation(-bgImageGap, 0, movementDuration);
bgImageOutRightAnimation = createBgImageOutAnimation(0, bgImageGap, movementDuration);
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
return NORMAL_ORDER[i];
}
private synchronized void setImageBitmapWithAnimation(Bitmap newBitmap, AnimationDirection animationDirection) {
if (animationDirection == AnimationDirection.LEFT) {
this.setInAnimation(bgImageInLeftAnimation);
this.setOutAnimation(bgImageOutLeftAnimation);
this.setImageBitmap(newBitmap);
} else if (animationDirection == AnimationDirection.RIGHT) {
this.setInAnimation(bgImageInRightAnimation);
this.setOutAnimation(bgImageOutRightAnimation);
this.setImageBitmap(newBitmap);
}
}
/*
* Call this method to update the background image.
* Here we are passing an image url and using Picasso
* downloading the image and then setting the background to it.
* We could also pass a drawable image or a drawable resource
* here if we wish.
* */
public void updateCurrentBackground(String imageUrl) {
this.currentAnimationDirection = AnimationDirection.RIGHT;
ImageView image = (ImageView) this.getNextView();
image.setImageDrawable(null);
showNext();
if(imageUrl == null) return;
Picasso.get().load(imageUrl)
.noFade().noPlaceholder()
.into(new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
setImageBitmapWithAnimation(bitmap, currentAnimationDirection);
}
@Override
public void onBitmapFailed(Exception e, Drawable errorDrawable) {
System.out.println("@#@#@#@#@" + e.getMessage());
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) { }
});
}
/*
* This method sets the Bitmap to the background.
*
* */
private void setImageBitmap(Bitmap bitmap) {
ImageView image = (ImageView) this.getNextView();
image.setImageDrawable(null);
int duration = 0;
animate().alpha(0.0f).setDuration(duration).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
image.setImageBitmap(bitmap);
new Handler().postDelayed(() -> animate().alpha(0.4f).setDuration(duration), 200);
}
@Override
public void onAnimationCancel(Animator animation) { }
@Override
public void onAnimationRepeat(Animator animation) { }
});
showNext();
}
/*
* Call this method used to clear the background image
* */
public void clearImage() {
ImageView image = (ImageView) this.getNextView();
image.setImageDrawable(null);
showNext();
}
public enum AnimationDirection {
LEFT, RIGHT
}
private static Animation createBgImageInAnimation(int fromX, int toX, int transitionDuration) {
TranslateAnimation translate = new TranslateAnimation(fromX, toX, 0, 0);
translate.setDuration(transitionDuration);
AnimationSet set = new AnimationSet(true);
set.setInterpolator(new DecelerateInterpolator());
set.addAnimation(translate);
return set;
}
private static Animation createBgImageOutAnimation(int fromX, int toX, int transitionDuration) {
TranslateAnimation ta = new TranslateAnimation(fromX, toX, 0, 0);
ta.setDuration(transitionDuration);
ta.setInterpolator(new DecelerateInterpolator());
return ta;
}
}
And that's it. We call this custom class from our xml file like this:
<com.example.BackgroundSwitcherView
android:id="@+id/overlay_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
And we can update the image of the BackgroundSwitcher view by calling this:
backgroundSwitcherView.updateCurrentBackground(movie.getPosterPath());