[Android] Tương Tác Các Thành Phần Trong Layout Khi Scroll List(Part 1)
Trong concept Material Design có nhiều tương tác kéo theo khi ngươi dùng thực hiện scroll một danh sách. Danh sách có thể là Listview, Gridview, RecycleView hoặc đơn giản là một ScrollView. Trong phần 1 này tôi trình bày cách làm để ẩn hiện thanh Toolbar/ActionBar khi scroll danh sách. Show ...
Trong concept Material Design có nhiều tương tác kéo theo khi ngươi dùng thực hiện scroll một danh sách. Danh sách có thể là Listview, Gridview, RecycleView hoặc đơn giản là một ScrollView.
Trong phần 1 này tôi trình bày cách làm để ẩn hiện thanh Toolbar/ActionBar khi scroll danh sách.
Show hide Toolbar khi scroll danh sách
Toolbar hoặc Actionbar có vị trí, thuộc tính được dùng trong phạm vi bài viết này tương tự nhau, vì thế tôi dùng từ Toolbar để chỉ cả 2 đối tượng.
Xem xét concept sau:
Phân tích:
- Người dùng scroll danh sách, thanh Toolbar ẩn khi vuốt listview đi lên, hiện ra khi listview đi xuống.
- Như vậy chúng ta có 2 việc quan trọng cần làm:
- Lắng nghe sự kiện list scroll
- Thực hiện ẩn, hiện thanh Toolbar theo sự kiện bắt được
1. Implement
Thư viện Thêm dòng sau vào build.gradle file
compile 'com.android.support:appcompat-v7:21.0.3 compile "com.android.support:recyclerview-v7:21.0.0" compile 'com.android.support:cardview-v7:21.0.3'`
Trong đó
- appcompat lib để tạo theme, toolbar
- recyclerview lib để tạo list
- cardview lib cho từng item trong list
** Layout **
List ở trường hợp này có thể dùng Listview hoặc RecycleView đều được. Trong bài viết này tôi sử dụng RecycleView để minh họa.
Thanh Toolbar có hiện đè lên phần layout của List, như vậy có thể có 2 cách xử lý: Dùng header hoặc padding cho RecycleView.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_awidth="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_awidth="match_parent" android:layout_height="match_parent"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_awidth="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary"/> <ImageButton android:id="@+id/fabButton" android:layout_awidth="56dp" android:layout_height="56dp" android:layout_gravity="bottom|right" android:layout_marginBottom="16dp" android:layout_marginRight="16dp" android:background="@drawable/fab_background" android:src="@drawable/ic_favorite_outline_white_24dp" android:contentDescription="@null"/> </FrameLayout>
Toolbar hiện overlay lên trên list nên ở đây ta chỉ có một lựa chọn là sử dụng FramgeLayout.
Tiếp theo tạo HomeActivity làm các công việc sau:
- Khởi tạo Toolbar
- Khởi tạo RecyclerView
- Khởi tạo FAB button
- Tạo Adapter cho RecyclerView
Các công việc trên làm như locgic thông thường, ở đây chưa có gì đặc biệt cả nên tôi không trình bày chi tiết. Các bạn có thể xem code trong github repo ở link cuối bài viết.
Đến đây bạn đã có một danh sách có thể scroll với một thanh toolbar và FAB button. Sang giai đoạn implement animation của Material Design.
Thực hiện test những gì vừa làm được, bạn có phát hiện thấy điều gì bất ổn không?
Một phần của list bị che bởi toolbar, do chúng ta đã sử dụng FrameLayout. Giải quyết vấn đề này bằng cách thêm padding cho RecycleView như sau:
<android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_awidth="match_parent" android:layout_height="match_parent" android:paddingTop="?attr/actionBarSize" android:clipToPadding="false"/>
Chạy lại và kiểm tra xem list có bị che nữa không.
Done!.
Tuy nhiên còn một cách nữa để fix, là thêm header cho RecyclerView
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { //added view types private static final int TYPE_HEADER = 2; private static final int TYPE_ITEM = 1; private List<String> mItemList; public RecyclerAdapter(List<String> itemList) { mItemList = itemList; } //modified creating viewholder, so it creates appropriate holder for a given viewType @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); if (viewType == TYPE_ITEM) { final View view = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false); return RecyclerItemViewHolder.newInstance(view); } else if (viewType == TYPE_HEADER) { final View view = LayoutInflater.from(context).inflate(R.layout.recycler_header, parent, false); return new RecyclerHeaderViewHolder(view); } throw new RuntimeException("There is no type that matches the type " + viewType + " + make sure your using types correctly"); } //modifed ViewHolder binding so it binds a correct View for the Adapter @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (!isPositionHeader(position)) { RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder; String itemText = mItemList.get(position - 1); // we are taking header in to account so all of our items are correctly positioned holder.setItemText(itemText); } } //our old getItemCount() public int getBasicItemCount() { return mItemList == null ? 0 : mItemList.size(); } //our new getItemCount() that includes header View @Override public int getItemCount() { return getBasicItemCount() + 1; // header } //added a method that returns viewType for a given position @Override public int getItemViewType(int position) { if (isPositionHeader(position)) { return TYPE_HEADER; } return TYPE_ITEM; } //added a method to check if given position is a header private boolean isPositionHeader(int position) { return position == 0; } }
OK, Code đã chạy tốt hơn, bây giờ sang phần ẩn/ hiện bar khi scroll.
Tạo một abstract class để nhận sự kiện onscroll của Recyclerview
public abstract class HidingScrollListener extends RecyclerView.OnScrollListener { private static final int HIDE_THRESHOLD = 20; private int scrolledDistance = 0; private boolean controlsVisible = true; @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { onHide(); controlsVisible = false; scrolledDistance = 0; } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { onShow(); controlsVisible = true; scrolledDistance = 0; } if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) { scrolledDistance += dy; } } public abstract void onHide(); public abstract void onShow(); }
Trong hàm onScrolled(), giá trị dx, dy trả về value scroll theo chiều ngang, chiều dọc của RecyclerView. Tuy nhiên giá trị này là độ sai khác của 2 dịch chuyển liên tiếp chứ không phải tổng khoảng cách scroll được. Tưởng tượng giống như dx, dy của tích phân.
Do đó để tính được tổng khoảng đã scroll, ta cần làm thế này
if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) { scrolledDistance += dy; }
Tính được khoảng dịch chuyển, tuy nhiên mỗi dx, dy là rất nhỏ. Ta cần một ngưỡng để xác định biên tối thiểu để ghi nhận đã scroll, ngưỡng này thường gọi là Threshold.
Khoảng dịch chuyển đã xong, bây giờ ta cần thông tin về hướng dịch chuyển nữa là xong. Giá trị dy>0 có nghĩa là dịch chuyển xuống, dy<0 là dịch chuyển lên
if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { onHide(); controlsVisible = false; scrolledDistance = 0; } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { onShow(); controlsVisible = true; scrolledDistance = 0; }
Tạo RecyclerVew
private void initRecyclerView() { RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); RecyclerAdapter recyclerAdapter = new RecyclerAdapter(createItemList()); recyclerView.setAdapter(recyclerAdapter); //setting up our OnScrollListener recyclerView.setOnScrollListener(new HidingScrollListener() { @Override public void onHide() { hideViews(); } @Override public void onShow() { showViews(); } }); }
Phần animation cho ẩn hiện view
private void hideViews() { mToolbar.animate().translationY(-mToolbar.getHeight()).setInterpolator(new AccelerateInterpolator(2)); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mFabButton.getLayoutParams(); int fabBottomMargin = lp.bottomMargin; mFabButton.animate().translationY(mFabButton.getHeight()+fabBottomMargin).setInterpolator(new AccelerateInterpolator(2)).start(); } private void showViews() { mToolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)); mFabButton.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)).start(); }
Build và test app. Đã chạy, bạn đã hoàn thành implement một concept Material Design đơn giản.
Trong phần 2 của bài viết tôi sẽ giải thích cụ thể cách các view trên làm việc và đi sâu vào phân tích ẩn/hiện/neo toolbar khi scroll danh sách.
Tham khảo Github reposity
https://github.com/mzgreen/HideOnScrollExample
Coding like a charm now!