[Android] Tương Tác Các Thành Phần Trong Layout Khi Scroll List(Part 1)
Bài đăng này đã không được cập nhật trong 3 năm
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, toolbarrecyclerview
lib để tạo listcardview
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_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"/>
<ImageButton
android:id="@+id/fabButton"
android:layout_width="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_width="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!
All rights reserved