android

Android Shared Element Transition cho Photo app – phần 2

Level 1 – Shared element từ RecyclerView sang 1 ImageView

  • Share Element từ 1 item của RecyclerView sang 1 ImageView trong Activity
  • 2 ImageView cùng load 1 ảnh



Define GalleryItem

class GalleryItem chứa thông tin cho 1 ảnh trong gallery, bao gồm: Id, link ảnh thumbnail và link ảnh full

public class GalleryItem implements Parcelable {

    private String mId;

    private String mThumbnailImg;
    private String mOriginalImg;

    // Other implementation...
}

Id của GalleryItem sẽ được dùng làm transitionName cho ImageView tương ứng.

Khởi tạo RecyclerView tại Activity 1

mPhotoList.setHasFixedSize(false);
mPhotoList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
mPhotoList.setAdapter(new RecyclerViewGalleryAdapter());

Tạo ViewHolder chứa 1 ảnh

static class ViewHolder extends RecyclerView.ViewHolder {
    ImageView mPhotoImg;

    ViewHolder(View itemView) {
        super(itemView);

        mPhotoImg = itemView.findViewById(R.id.photo_img);
    }
}

Khai báo RecyclerViewGalleryAdapter. Mỗi ảnh được gán transitionName bằng với Id của GalleryItem

class RecyclerViewGalleryAdapter extends RecyclerView.Adapter<ViewHolder> {
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(getActivity()).inflate(R.layout.layout_poster_list_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final @NonNull ViewHolder holder, int position) {
        final GalleryItem galleryItem = mGalleryItems.get(position);

        // Set the transition name by gallery's id to each ImageView
        ViewCompat.setTransitionName(holder.mPhotoImg, String.valueOf(galleryItem.getId()));

        // Load the thumbnail image to the list, cache it into memory and data so the fullscreen thumbnail can be loaded faster
        Glide.with(activity)
                .load(Uri.parse(galleryItem.getThumbnailImg()))
                .apply(new RequestOptions().skipMemoryCache(false).diskCacheStrategy(DiskCacheStrategy.DATA))
                .into(holder.mPhotoImg);

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Show fullscreen activity with single photo
                Intent intent = new Intent(activity, LevelOneFullPhotoActivity.class);
                intent.putExtra(LevelOneFullPhotoActivity.PHOTO_GALLERY_ITEM, galleryItem);

                ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, holder.mPhotoImg, ViewCompat.getTransitionName(holder.mPhotoImg));
                activity.startActivity(intent, options.toBundle());
                activity.finish();
            }
        });
    }

    @Override
    public int getItemCount() {
        return mGalleryItems != null ? mGalleryItems.size() : 0;
    }
}

Như đoạn code trên, mình dùng Glide để load ảnh kèm theo option cache ảnh vào memory để giảm thời gian giảm thời gian load ảnh ở Acitvity 2.

Hiển thị ảnh full ở Activity 2

Tại Activity 2, ta nhận GalleryItem được truyền tới và load ảnh:

Bundle data = getIntent().getExtras();
if (data != null) {
    mGallery = data.getParcelable(PHOTO_GALLERY_ITEM);

    // Postpone the Shared Element transition to wait until image is fully loaded
    supportPostponeEnterTransition();

    // Set the transition name from the selected item
    ViewCompat.setTransitionName(mPhotoImg, String.valueOf(mGallery.getId()));
}

loadImg();

Trước khi bắt đầu gọi loadImg(), ta dùng hàm supportPostponeEnterTransition() để tạm dừng transition. Do ảnh sẽ bị delay 1 khoảng thời gian trước khi được load xong, để transition chạy lúc này sẽ gặp hiện tượng chớp, giật và transition sẽ không có tác dụng.

Sau khi load ảnh xong, gọi hàm supportStartPostponedEnterTransition để tiếp tục transition đang được tạm ngưng.

private void loadImg() {
    Glide.with(this)
            .load(Uri.parse(mGallery.getThumbnailImg()))
            .apply(new RequestOptions().dontAnimate().skipMemoryCache(false).diskCacheStrategy(DiskCacheStrategy.DATA))
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                    // Continue the transition
                    supportStartPostponedEnterTransition();

                    return false;
                }

                @Override
                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                    // Continue the transition
                    supportStartPostponedEnterTransition();

                    Toast.makeText(LevelOneFullPhotoActivity.this, "Load image fail", Toast.LENGTH_SHORT).show();

                    return false;
                }
            })
            .into(mPhotoImg);
}

Lưu ý:

  • Nhớ thêm option dontAnimate vào Glide để các animation không overlap nhau
  • Nếu đã gọi supportPostponeEnterTransition() nhưng không gọi supportStartPostponedEnterTransition() thì màn hình trắng trống trơn và stuck luôn ở đó, nên luuôn nhớ phải supportStartPostponedEnterTransition ngay cả khi xử lý thành công hay thất bại.

Kết quả Level 1:

Shared element 2 result
Shared element 2 result

Level này cũng không thực sự quá khó. Ta khởi tạo và load ảnh cho từng item trong RecyclerView cũng tương tự như Level 0.

Vấn đề xảy ra với Level này là ta cùng load 1 ảnh và khi ở trạng thái full screen sẽ dễ dàng bị vỡ. Đây là tiền đề tới Level 2

Phần 3: load 2 ảnh khác nhau – Thumbnail ở Activity 1 và Full ở Activity 2

Phần 3: Share element level 2


Full demo app

Hỏi tôi vài câu được hok?

Website này sử dụng Akismet để hạn chế spam. Tìm hiểu bình luận của bạn được duyệt như thế nào.