I'm working on a project where I want to add a View
transition like below. I have no idea where to start, can anybody help me out?

- 30,484
- 10
- 122
- 151

- 41
- 6
-
What you tried so far ? – IntelliJ Amiya Sep 21 '17 at 05:58
-
I just created the initial three custom views....as I said, I have no idea about how can I go from the first layout to second layout with that smooth view transition. – Shomen Das Sep 21 '17 at 06:05
-
Try these as well - https://github.com/wasabeef/awesome-android-ui – Sanved Sep 21 '17 at 06:39
1 Answers
There are several ways you could achieve this type of effect, no idea how that vocabulary app is handling it, but a pretty easy way to get a very similar effect is to use multiple RecyclerView.ViewHolder
types and then let DefaultItemAnimator
take care of the animation work. Here's one way to go about that:
Model
Our model is going to contain the data we're display as well as a type
to inform our RecyclerView.Adapter
which ReclerView.ViewHolder
to inflate. So, it might look something like this (AutoValue):
@AutoValue
public abstract class ExpandableModel {
public static final int TYPE_STATIC = 0;
public static final int TYPE_EXPANDED = 1;
public static final int TYPE_COLLAPSED = 2;
@Nullable public abstract List<ExpandableModel> data();
public abstract String title();
public abstract int progress();
public abstract int max();
public abstract int type();
public static ExpandableModel createExpanded(List<ExpandableModel> data,
String title, int progress, int max) {
return new AutoValue_ExpandableModel(data, title, progress, max, TYPE_EXPANDED);
}
public static ExpandableModel createCollapsed(List<ExpandableModel> data,
String title, int progress, int max) {
return new AutoValue_ExpandableModel(data, title, progress, max, TYPE_COLLAPSED);
}
public static ExpandableModel createExpanded(ExpandableModel model) {
return new AutoValue_ExpandableModel(
model.data(), model.title(), model.progress(), model.max(), TYPE_EXPANDED);
}
public static ExpandableModel createCollapsed(ExpandableModel model) {
return new AutoValue_ExpandableModel(
model.data(), model.title(), model.progress(), model.max(), TYPE_COLLAPSED);
}
public static ExpandableModel createStatic(String title, int progress, int max) {
return new AutoValue_ExpandableModel(null, title, progress, max, TYPE_STATIC);
}
}
ViewHolder
We can define a basic ReclerView.ViewHolder
that will bind some ExpandableModel
data as well as provide us with a nice OnClickListener
callback.
public abstract class ExpandableViewHolder extends RecyclerView.ViewHolder {
public ExpandableViewHolder(ViewGroup parent, int layout) {
super(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
}
public void setItemClickListener(OnItemClickListener clickListener) {
itemView.setOnClickListener(v -> {
final int adapterPosition = getAdapterPosition();
if (adapterPosition != RecyclerView.NO_POSITION) {
clickListener.onItemClick(itemView, adapterPosition);
}
});
}
public abstract void bind(ExpandableModel model);
public interface OnItemClickListener {
void onItemClick(View itemView, int position);
}
}
ExpandedViewHolder
public class ExpandedViewHolder extends ExpandableViewHolder {
private final TextView title;
private final TextView completion;
private final ProgressBar progress;
private final RecyclerView recycler;
public ExpandedViewHolder(ViewGroup parent) {
super(parent, R.layout.adapter_view_expanded);
title = itemView.findViewById(R.id.expanded_category);
completion = itemView.findViewById(R.id.expanded_completion);
progress = itemView.findViewById(R.id.expanded_progress);
recycler = itemView.findViewById(R.id.expanded_recycler);
recycler.addItemDecoration(new SpaceItemDecoration(10));
}
@Override
public void bind(ExpandableModel model) {
title.setText(model.title());
completion.setText(model.progress() + "/" + model.max());
progress.setMax(model.max());
progress.setProgress(model.progress());
recycler.setAdapter(new ExpandableAdapter(model.data()));
}
}
ExpandedViewHolder layout
<LinearLayout 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:layout_width="match_parent"
android:layout_height="350dp"
android:background="#ffFFC857"
android:orientation="vertical">
<TextView
android:id="@+id/expanded_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textColor="#ffffffff"
android:textIsSelectable="false"
android:textSize="28sp"
tools:text="Basic Words" />
<TextView
android:id="@+id/expanded_completion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColor="#ffffffff"
android:textIsSelectable="false"
android:textSize="18sp"
tools:text="174/174 mastered" />
<ProgressBar
android:id="@+id/expanded_progress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
tools:progress="100" />
<android.support.v4.widget.Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<android.support.v7.widget.RecyclerView
android:id="@+id/expanded_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="4dp"
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
</LinearLayout>
CollapsedViewHolder
public class CollapsedViewHolder extends ExpandableViewHolder {
private final TextView title;
private final TextView completion;
private final ProgressBar progress;
public CollapsedViewHolder(ViewGroup parent) {
super(parent, R.layout.adapter_view_collapsed);
title = itemView.findViewById(R.id.collapsed_category);
completion = itemView.findViewById(R.id.collapsed_completion);
progress = itemView.findViewById(R.id.collapsed_progress);
}
@Override
public void bind(ExpandableModel model) {
title.setText(model.title());
completion.setText(model.progress() + "/" + model.max());
progress.setMax(model.max());
progress.setProgress(model.progress());
}
}
CollapsedViewHolder layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="165dp"
android:layout_margin="4dp"
android:background="#ffffffff"
android:orientation="vertical">
<TextView
android:id="@+id/collapsed_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textColor="#ff066FA5"
android:textIsSelectable="false"
android:textSize="28sp"
tools:text="Basic Words" />
<TextView
android:id="@+id/collapsed_completion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColor="#ffAEB8C3"
android:textIsSelectable="false"
android:textSize="18sp"
tools:text="174/174 mastered" />
<ProgressBar
android:id="@+id/collapsed_progress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
tools:progress="100" />
</LinearLayout>
Adapter
Now we can create our RecyclerView.Adapter
. Basically, whenever an item is clicked we're going to replace it with either a TYPE_EXPANDED
or TYPE_COLLAPSED
ExpandableModel
and because DefaultItemAnimator
is already applied to RecyclerView
, calling RecyclerView.Adapter.notifyItemChanged
will nicely animate between the two types of RecyclerView.ViewHolder
.
public class ExpandableAdapter extends RecyclerView.Adapter<ExpandableViewHolder> {
private final List<ExpandableModel> data = new ArrayList<>(0);
private int expandedPosition;
public ExpandableAdapter(Collection<ExpandableModel> data) {
this.data.addAll(data);
}
@Override
public ExpandableViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_EXPANDED:
final ExpandedViewHolder expandedHolder = new ExpandedViewHolder(parent);
expandedHolder.setItemClickListener((itemView, position) -> collapse(position));
return expandedHolder;
case TYPE_COLLAPSED:
final CollapsedViewHolder collapsedHolder = new CollapsedViewHolder(parent);
collapsedHolder.setItemClickListener((itemView, position) -> {
collapseCurrent();
expand(position);
});
return collapsedHolder;
case TYPE_STATIC:
final CollapsedViewHolder staticHolder = new CollapsedViewHolder(parent);
staticHolder.setItemClickListener((itemView, position) -> {
final ExpandableModel model = data.get(position);
Snackbar.make(itemView, model.title(), Snackbar.LENGTH_SHORT).show();
});
return staticHolder;
default:
throw new IllegalArgumentException("unknown type");
}
}
@Override
public void onBindViewHolder(ExpandableViewHolder holder, int position) {
holder.bind(data.get(holder.getAdapterPosition()));
}
@Override
public int getItemCount() {
return data.size();
}
@Override
public int getItemViewType(int position) {
return data.get(position).type();
}
private void collapseCurrent() {
final ExpandableModel curr = data.get(expandedPosition);
data.set(expandedPosition, ExpandableModel.createCollapsed(curr));
notifyItemChanged(expandedPosition);
}
private void collapse(int position) {
final ExpandableModel curr = data.get(position);
data.set(position, ExpandableModel.createCollapsed(curr));
notifyItemChanged(position);
}
private void expand(int position) {
final ExpandableModel curr = data.get(position);
data.set(position, ExpandableModel.createExpanded(curr));
notifyItemChanged(position);
expandedPosition = position;
}
}
Dummy data
final Random ran = new SecureRandom();
final List<ExpandableModel> basic = new ArrayList<>(0);
for (int i = 0; i < 10; i++) {
final int max = 10;
final int progress = ran.nextInt(max + 1);
final String title = ("Basic Words: " + (i + 1));
basic.add(ExpandableModel.createStatic(title, progress, max));
}
final List<ExpandableModel> intermediate = new ArrayList<>(0);
for (int i = 0; i < 10; i++) {
final int max = 10;
final int progress = ran.nextInt(max + 1);
final String title = ("Intermediate Words: " + (i + 1));
intermediate.add(ExpandableModel.createStatic(title, progress, max));
}
final List<ExpandableModel> advanced = new ArrayList<>(0);
for (int i = 0; i < 10; i++) {
final int max = 10;
final int progress = ran.nextInt(max + 1);
final String title = ("Advanced Words: " + (i + 1));
advanced.add(ExpandableModel.createStatic(title, progress, max));
}
final List<ExpandableModel> data = new ArrayList<>(0);
data.add(ExpandableModel.createCollapsed(basic, "Basic Words", 7, 10));
data.add(ExpandableModel.createCollapsed(intermediate, "Intermediate Words", 5, 10));
data.add(ExpandableModel.createCollapsed(advanced, "Advanced Words", 3, 10));
final RecyclerView recycler = findViewById(android.R.id.list);
recycler.setAdapter(new ExpandableAdapter(data));
Extra
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private final int space;
public SpaceItemDecoration(int space) {
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
final int childPosition = parent.getChildLayoutPosition(view);
if (childPosition == RecyclerView.NO_POSITION) {
return;
}
if (childPosition < 1 || childPosition >= 1) {
outRect.left = space;
}
if (childPosition == getTotalItemCount(parent) - 1) {
outRect.right = space;
}
}
private static int getTotalItemCount(RecyclerView parent) {
return parent.getAdapter().getItemCount();
}
}
Results (video)

- 30,484
- 10
- 122
- 151