12/08/2018, 12:49

[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:

demo_gif.gif

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:
  1. Lắng nghe sự kiện list scroll
  2. 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!

0