[Android] Carousel Background Parallax View

[Android] Carousel Background Parallax View

Google Play Carousel Background Parallax View

I saw this cool view in the Play Store for quite sometimes now and cannot help myself of creating one. Let’s begin…

Breaking down view layers

The view is broken down into four layers.

  • Layer 1: Padded Card View
  • Layer 2: A Parallax Image View that fade-out when recycler view scroll towards the left and fade-in when recycler view scroll to the right
  • Layer 3: A Color View that fade-in when recycler view scroll towards the left and fade-out when recycler view scroll to the right
  • Layer 4: Content [Header + Recycler View with invisible start item and snapping effect]

Clone the project or checkout from version control in Android Studio

From Gitlab
$ git clone https://gitlab.com/apdevboost/bg-parallax-carousel-view.git
OR From Github
$ git clone https://github.com/ApDevBoost/carousel-bg-parallax-view.git

Understanding the code

  1. Since we are making the view as dynamic as possible, the view data including its view type is got from API https://www.mocky.io/v2/5ac4a8192f00003600f5faed.
  2. In order to achieve the parallax effect, the image must be slightly out of the view to provide room for motion. In this case, I set the image view to start -40dp and ends at -40dp so that we do not have image cut-off at the end while translating.
            android:id="@+id/viewholder_carousel_bg_parallax_bg_iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"
android:layout_marginStart="-40dp"
android:layout_marginEnd="-40dp"/>
Background ImageView

So, the xml code for viewholder_carousel_bg_parallax_bg.xml is


xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">

android:layout_width="match_parent"
android:layout_height="232dp" >


android:id="@+id/viewholder_carousel_bg_parallax_bg_iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/placeholder"
android:layout_marginStart="-40dp"
android:layout_marginEnd="-40dp"/>


android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/base_color_black_alpha_30"/>


android:id="@+id/viewholder_carousel_bg_parallax
_alpha_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:alpha="0"/>



android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

android:id="@+id/viewholder_carousel_bg_parallax_li"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="16dp">
            
android:id="@+id/viewholder_carousel_bg_parallax
_title_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="15sp"
android:textColor="@color/base_color_white"
android:textStyle="bold"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:textSize="11sp"
android:textColor="@color/base_color_white"
android:text="@string/view_all"
android:textStyle="bold"/>



android:id="@+id/viewholder_carousel_bg_parallax_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="4dp"
android:clipToPadding="false"
android:paddingBottom="16dp"/>


3. Set Recycler View to have horizontal orientation and implement snapping behavior [I use ‘com.github.rubensousa:gravitysnaphelper:1.5’ library to get the gravity snapping effect].There are two types of items in the view

  • The invisible item and
  • The visible item

The XML,ViewHolders and Adapters for these items are

XML

Invisible item view: viewholder_carousel_invisible_item.xml


xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@android:color/white"
android:layout_width="200dp"
android:layout_height="wrap_content"
app:cardPreventCornerOverlap="false">

Visible item view: viewholder_carousel_item.xml


xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/viewholder_carousel_item_ll"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/selectableItemBackground"
android:paddingRight="4dp"
android:paddingLeft="4dp">

android:background="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardPreventCornerOverlap="false"
app:cardElevation="@dimen/cardview_default_elevation"
app:cardCornerRadius="8dp">

android:id="@+id/viewholder_carousel_item_cover_iv"
android:layout_width="92dp"
android:layout_height="92dp"
android:scaleType="centerCrop"/>


android:id="@+id/viewholder_carousel_item_title_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"/>

android:id="@+id/viewholder_carousel_item_subTitle_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"/>

ViewHolders

Carousel ViewHolder: CarouselViewHolder.java

public class CarouselViewHolder extends RecyclerView.ViewHolder{
public ImageView bgIv;
public RecyclerView horizontalRv;
public TextView titleTv;
public LinearLayout li;
public CarouselItemAdapter gridAdapter;
public LinearLayoutManager linearLayoutManager;
private List items;
public CarouselViewHolder(View itemView) {
super(itemView);
horizontalRv = (RecyclerView) itemView.findViewById(R.id.viewholder_carousel_rv);
titleTv = (TextView) itemView.findViewById(R.id.viewholder_carousel_title_tv);
li = (LinearLayout) itemView.findViewById(R.id.viewholder_carousel_li);
bgIv = itemView.findViewById(R.id.viewholder_carousel_bg_iv);
linearLayoutManager = new LinearLayoutManager(itemView.getContext(), LinearLayoutManager.HORIZONTAL, false);
horizontalRv.setLayoutManager(linearLayoutManager);
horizontalRv.setHasFixedSize(true);
horizontalRv.setNestedScrollingEnabled(false);

//Adding snapping effect onto recycler view
SnapHelper snapHelperStart = new GravitySnapHelper(Gravity.START);
snapHelperStart.attachToRecyclerView(horizontalRv);
}
public void setCarouselAdapter(Context context, List items, boolean isInvisibleStart) {
this.items = items;
gridAdapter = new CarouselItemAdapter(context, isInvisibleStart);
gridAdapter.setDao(items);
horizontalRv.setAdapter(gridAdapter);
}

public void updateGridAdapter(List itemsLists) {
this.items = itemsLists;
gridAdapter.setDao(items);
gridAdapter.notifyDataSetChanged();
}
}

Invisible item ViewHolder: CarouselInvisibleItemViewHolder.java

public class CarouselInvisbileItemViewHolder extends RecyclerView.ViewHolder{
public CarouselInvisbileItemViewHolder(View itemView) {
super(itemView);

}
}

Visible item ViewHolder: CarouselItemViewHolder.

public class CarouselItemViewHolder extends RecyclerView.ViewHolder{
public SquareImageView ivCover;
public TextView titleTv;
public TextView subTitleTv;
public LinearLayout ll;
public CarouselItemViewHolder(View itemView) {
super(itemView);
ll = (LinearLayout) itemView.findViewById(R.id.viewholder_carousel_item_ll);
ivCover = (SquareImageView) itemView.findViewById(R.id.viewholder_carousel_item_cover_iv);
titleTv = (TextView) itemView.findViewById(R.id.viewholder_carousel_item_title_tv);
subTitleTv = (TextView) itemView.findViewById(R.id.viewholder_carousel_item_subTitle_tv);

}
}

Adapters

Carousel Item View Adapter: CarouselItemAdapter.java

public class CarouselItemAdapter extends RecyclerView.Adapter {

private List items;
private Context context;
private boolean isInvisible;

private final static int INVISIBLE_VIEW_TYPE = 100;
public CarouselItemAdapter(Context context, boolean isInvisibleStart) {
this.context = context;
this.isInvisible= isInvisibleStart;
}

public void setDao(List items) {
this.items = items;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == INVISIBLE_VIEW_TYPE)
return new CarouselInvisbileItemViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.viewholder_carousel_invisible_item, parent, false));
else
return new CarouselItemViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.viewholder_carousel_item, parent, false));
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof CarouselItemViewHolder) {
if (isInvisible)
position=position-1;
final Item item = items.get(position);
CarouselItemViewHolder vh = (CarouselItemViewHolder) holder;
GlideApp.with(context)
.load(item.getCoverUrl())
.placeholder(R.drawable.place_holder)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(vh.ivCover);
vh.titleTv.setText(item.getTitle());
vh.subTitleTv.setText(item.getSubTitle());


vh.ll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});
}

}

@Override
public int getItemCount() {
if (items == null)
return 0;
if (isInvisible)
return items.size()+1;
return items.size();
}

@Override
public int getItemViewType(int position) {
if (isInvisible && position==0)
return INVISIBLE_VIEW_TYPE;
return super.getItemViewType(position);
}
}

Carousel View Adapter: CarouselAdapter.java

public class CarouselAdapter extends RecyclerView.Adapter {

private ItemsListModel itemsListModel = null;
private Context context;
private final static int CAROUSEL_PARALLAX_BG_VIEW_TYPE = 100;
public CarouselAdapter(Context context, ItemsListModel itemsListModel) {
this.context = context;
this.itemsListModel = itemsListModel;
}

public void setDao(ItemsListModel itemsListModel) {
this.itemsListModel = itemsListModel;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new CarouselBgParallaxViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.viewholder_carousel_bg_parallax, parent, false));
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ItemsList itemsList = itemsListModel.getItemsList().get(position);

if(holder instanceof CarouselBgParallaxViewHolder)
{
final CarouselBgParallaxViewHolder vh = (CarouselBgParallaxViewHolder) holder;
vh.titleTv.setText(itemsList.getItemTitle());

if (vh.gridAdapter == null) {
GlideApp.with(context)
.load(itemsList.getItemBgUrl())
.placeholder(R.drawable.place_holder)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.centerCrop()
.into(vh.bgIv);
vh.alphaView.setBackgroundColor(Color.parseColor(itemsList.getItemFgColor()));
vh.horizontalRv.setOnScrollListener(new RecyclerView.OnScrollListener() {

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);

// Check if the the visible item is the first item
if (vh.linearLayoutManager.findFirstVisibleItemPosition()==0)
{
// Get view of the first item
View firstVisibleItem = vh.linearLayoutManager.findViewByPosition(vh.linearLayoutManager.findFirstVisibleItemPosition());
float distanceFromLeft = firstVisibleItem.getLeft(); // distance from the left
float translateX = (int)distanceFromLeft * 0.2f; // move x distance

vh.bgIv.setTranslationX(translateX);

// If the view scroll pass the starting position, change color view alpha
if (distanceFromLeft <= 0) {
float itemSize = firstVisibleItem.getWidth(); // view size
float alpha = (Math.abs(distanceFromLeft) / itemSize * 0.80f); // view transparency

//Set alpha to image to bring 'fade out' and 'fade in' effect
vh.bgIv.setAlpha(1 - alpha);
//Set alpha to color view to bring 'darker' and 'clearer' effect
vh.alphaView.setAlpha(alpha);
}
}
}
});
vh.setCarouselAdapter(context, itemsList.getItems(), true);

} else {
vh.updateGridAdapter(itemsList.getItems());
}
vh.li.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//TODO: Add on click here
}
});
}

}


@Override
public int getItemCount() {
if (itemsListModel == null)
return 0;
else if (itemsListModel.getItemsList() == null)
return 0;
return itemsListModel.getItemsList().size();
}

@Override
public int getItemViewType(int position) {
return CAROUSEL_PARALLAX_BG_VIEW_TYPE;
}


}

Pay attention to the below code block in CarouselAdapter.java. Since the parallax effect takes place only when we scroll the invisible item, we check for the current visible item to be the first invisible item [vh.linearLayoutManager.findFirstVisibleItemPosition()==0]. Next, we translate background images corresponding to how far the invisible item is far from starting position of recycler view. As we scroll the invisible item towards the left, the distanceFromLeft values decrease from 48,46…,0,-1,-2 …,-200,… and increase back to 48 when moving back to its original position. In order to control slow and smooth translation movement, 0.2f is multiplied to distanceFromLeft. Lastly, the fade-in and fade-out effects of background image and color view are controlled by an alpha percentage [calculated by Math.abs(distanceFromLeft)/invisibleViewItemSizeshould] . Note that, alpha is being applied only after distanceFromLeft ≤ 0 otherwise the ‘background + color view’ will not have continuous bi-directional fade-in and fade-out flow as Math.abs(distanceFromLeft) will result all positive numbers [ 48,…0,….200,…] from [48,46…,0,-1,-2 …,-200,…] and we want only [0,…200,…]. As the the color view fade-in [alpha], the background image fade-out [1-alpha]

vh.horizontalRv.setOnScrollListener(new RecyclerView.OnScrollListener() {

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);

// Check if the the visible item is the first item
if (vh.linearLayoutManager.findFirstVisibleItemPosition()==0)
{
// Get view of the first item
View firstVisibleItem = vh.linearLayoutManager.findViewByPosition(vh.linearLayoutManager.findFirstVisibleItemPosition());
float distanceFromLeft = firstVisibleItem.getLeft(); // distance from the left
float translateX = (int)distanceFromLeft * 0.2f; // move x distance

vh.bgIv.setTranslationX(translateX);

// If the view scroll pass the starting position, change color view alpha
if (distanceFromLeft <= 0) {
float itemSize = firstVisibleItem.getWidth(); // view size
float alpha = (Math.abs(distanceFromLeft) / itemSize * 0.80f); // view transparency

//Set alpha to image to bring 'fade out' and 'fade in' effect
vh.bgIv.setAlpha(1 - alpha);
//Set alpha to color view to bring 'darker' and 'clearer' effect
vh.alphaView.setAlpha(alpha);
}
}
}
});

Tips on choosing image and foreground view color

Try to choose an image with distinct characters on the left and little unclear or less characteristics on the right. We want the least visible because we do not want users to overlook the recycler view items. For the variable alpha view color, select the color similar to the right blurry portion. When we scroll, the variable alpha effect will be as if it emerges from and recedes back to the right.

Result

Carousel-Background-Parallax-View

This is my first Article. Please clap and comment what improvement or views would you like me to make in the future. Feel free to clone the project and check it out.

“[Android] Carousel Background Parallax View” Posted first on ” Android on Medium “
Author: AP Development

Author: Pawan Kumar

Leave a Reply

Close Menu
%d bloggers like this:
Skip to toolbar