2

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?

adneal
  • 30,484
  • 10
  • 122
  • 151
Shomen Das
  • 41
  • 6

1 Answers1

2

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)

adneal
  • 30,484
  • 10
  • 122
  • 151