Snackbar (Bài trước)
(Bài tiếp) RecylerView

Giới thiệu về Floating Action Button (FAB)

FAB là một nút bấm đặc biệt, chúng thường được biểu diễn bởi hình tròn với biểu tượng, nó nổi trên bề mặt các giao diện thiết kế, thường thì tùy ngữ cảnh bấm vào nó để thực hiện các hành động tương ứng.

Nguyên tắc thiết kế giao diện có sử dụng FAB theo triết lý Materail Design được Google đề xuất ở đây: FAB Design

Thường thì bạn sử dụng FAB tùy thuộc ngữ cảnh, và FAB là thao tác chủ đạo đưa ra cho người dùng thực hiện. Ví dụ bạn đang ở màn hình xem danh sách email, có thể xuất hiện FAB để bấm vào đó tạo một Email mới (nếu cho rằng đây là thao tác người dùng hay sử dụng) ...

Kiếm Icon cho FAB

Ngoài cách tự thiết kế vẽ ICON sử dụng cho FAB, có rất nhiều ICON chuẩn của Google phù hợp tại material.io, bạn chỉ việc tìm Icon phù hợp, tải về và nhét vào res/drawable của dự án và sử dụng.

Bạn có thể tải định dạng svg sau đó dùng SVG -> XML Drawable để dễ dùng hơn.

Thư viện sử dụng

Để sử dụng FAB bạn cần thêm vào build.gradle thư viện: compile 'com.android.support:design:X.X.X' với X là phiên bản muốn dùng. Ngoài ra cũng nên thêm các thành phần để sử dụng cùng FAB như recyclerview ...

Tóm lại bạn kiểm tra build.gradle và thêm các thư viện như thế này vào:

compile 'com.android.support:appcompat-v7:26.1.0'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:design:26.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'

Sử dụng FAB

FAB có thể thêm vào các file layout với cú pháp dạng:

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:src="@drawable/face"
    app:fabSize="normal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

Với fabSize có thể nhận giá trị normal thì FAB ở cỡ chuẩn, và giá trị mini FAB ở cỡ nhỏ.

Để định vị FAB một cách linh động, chúng ta sẽ sử dụng CoordinatorLayout để trình bày Layout, vì loại View này giúp cho việc tương tác giữa các phần tử View con của nó được dễ dàng hơn, đặc biệt là các hiệu ứng động khi thực hiện cuốn các phần tử, phần này trình bày sau. Ngay bây giờ chúng ta sẽ sử dụng ưu điểm của CoordinatorLayout khi cho phép ta điều khiển trượt một .

Ví dụ đơn giản, sử dụng ListView, FloatingAcionButton bên trong CoordinatorLayout, lưu ý đến thuộc tính layout_anchorlayout_anchorGravity

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/mylistview"
        android:background="@android:color/holo_orange_light"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </ListView>


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:src="@drawable/face"
        app:fabSize="normal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/mylistview"
        app:layout_anchorGravity="bottom|right|end" />

</android.support.design.widget.CoordinatorLayout>

Code để ListView hiện thị một danh sách dữ liệu

public class FabActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fab);

        ListView listView = findViewById(R.id.mylistview);

        String[] arWords = new String[] {
                "Phần Tử 1", "Phần Tử 2", "Phần Tử 3", "Phần Tử 4",
                "Phần Tử 5", "Phần Tử 6", "Phần Tử 7",
                "Phần Tử 8", "Phần Tử 9", "Phần Tử 10", "Phần Tử 11",
                "Phần Tử 12", "Phần Tử 13", "Phần Tử 14",
                "Phần Tử 15", "Phần Tử 16", "Phần Tử 17", "Phần Tử 18",
                "Phần Tử 19", "Phần Tử 20", "Phần Tử 21",
                "Phần Tử 22", "Phần Tử 23", "Phần Tử 24", "Phần Tử 25"};

        ArrayAdapter adapter =
                new ArrayAdapter(this, android.R.layout.simple_list_item_1, arWords);

        listView.setAdapter(adapter);
    }
}
fab

Lưu ý: Thuộc tính sử dụng trong FAB

  • app:layout_anchor chỉ ra vị trí của FAB neo cùng với View nào, ví dụ trên là app:layout_anchor="@id/mylistview", tức là đố tượng hiện thị danh sách
  • app:layout_anchorGravity chỉ ra vị trí neo tương đối so với đối tượng vào, có nhiều giá trị có thể nhận như: top, left, right, buttom, start, end ... tương ứng khi neo vào đỉnh, bên trái, bên phải, phía dưới ...

Hiệu ứng động - Ẩn/Hiện Floating Action Button

Khi cuộn danh sách lên xuống, bạn muốn FAB ẩn đi/hiện ra khi kéo lên, xuống như dạng sau:

FAB Animate

Để thực hiện điều này, trước tiên việc hiện thị danh sách dùng ListView cần chuyển sang dùng RecyclerView, vì RecyclerView có hỗ trợ tương tác với các phần tử khác bên trong CoordinatorLayout

Chi tiết về sử dụng RecyclerView sẽ đề cập chi tiết riêng ở phần sau

Trong layout chuyển sang như sau:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/mylistview"
        android:background="@android:color/holo_orange_light"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:src="@drawable/face"
        app:fabSize="normal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/mylistview"
        app:layout_anchorGravity="bottom|right|end" />

</android.support.design.widget.CoordinatorLayout>

Tiếp thay vì dùng ArrayAdapter như ListView, RecyclerView phải dùng lớp kế thừa từ RecyclerView.Adapter, trong đó bạn phải xây dựng thêm lớp kế thừa từ RecyclerView.ViewHolder để biểu diễn View các phần tử trong danh sách. Mã chuyển đổi đầy đủ như sau:


public class FabActivity extends AppCompatActivity {

    
    /**
     * Lớp biểu diễn Holder trong RecycleView
     */
    class ElementViewHolder extends RecyclerView.ViewHolder
    {
        TextView textView;

        public ElementViewHolder(TextView itemView) {
            super(itemView);
            textView = itemView;
        }

        public TextView getTextView() {
            return textView;
        }
    }
        

    
    /**
     * Adapter cho danh sách phần tử Text dùng trong RecycleView
     */
    class ElementsAdapter extends RecyclerView.Adapter {

        LayoutInflater inflater = LayoutInflater.from(getBaseContext());
        String[] arWords = new String[] {
                "Phần Tử 1", "Phần Tử 2", "Phần Tử 3", "Phần Tử 4",
                "Phần Tử 5", "Phần Tử 6", "Phần Tử 7",
                "Phần Tử 8", "Phần Tử 9", "Phần Tử 10",
                "Phần Tử 11", "Phần Tử 12", "Phần Tử 13", "Phần Tử 14",
                "Phần Tử 15", "Phần Tử 16", "Phần Tử 17",
                "Phần Tử 18", "Phần Tử 19", "Phần Tử 20", "Phần Tử 21",
                "Phần Tử 22", "Phần Tử 23", "Phần Tử 24", "Phần Tử 25"};
        @Override
        public RecyclerView.ViewHolder
            onCreateViewHolder(ViewGroup parent, int viewType) {
                TextView v = (TextView)inflater
                    .inflate(android.R.layout.simple_list_item_1, parent, false);
            return new ElementViewHolder(v);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            ((ElementViewHolder)holder).getTextView().setText(arWords[position]);

        }

        @Override
        public int getItemCount() {
            return arWords.length;
        }

    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fab);

        
        //Sử dụng RecyclerView
        RecyclerView listView = findViewById(R.id.mylistview);
        RecyclerView.Adapter adapter = new  ElementsAdapter();
        listView.setLayoutManager(new LinearLayoutManager(this));

        listView.setAdapter(adapter);
        
    }
}

Chạy thử, thấy đã hiện thị và ứng xử tương đương với cách dùng ListView ở trên

fab

Bây giờ đặt giả thiết, FAB sẽ ẩn đi nếu vuốt ngược lên và hiện thị khi kéo danh sách về đầu, với yêu cầu chỉ hiện thị nếu phần tử đầu tiên của danh sách hiện thị (và chỉ ẩn đi nếu phần tử đầu tiên bị ẩn).

Tiến hành làm như sau:

Do FAB trong layout xây dựng bằng CoordinatorLayout nên nó có thể thiết lập thuộc tính app:layout_behavior để trỏ tới lớp xử lý ứng xử của FAB. Lớp này bạn xây dựng bằng kế thừa FloatingActionButton.Behavior, trong đó quan trọng bạn sẽ quá tải phương thức onNestedScroll để xử lý FAB mỗi khi RecyclerView cuộn lên xuống. Giả xử lớp này bạn xây dựng có tên MyFABAnimate thì nội dung code như sau:

package net.xuanthulab.floatactionbuttondemo; //thay bằng package khác của riêng bạn


public class MyFABAnimate extends FloatingActionButton.Behavior {
    private String TAG = "MyFABAnimate";

    //Không có phương thực này sẽ lỗi
    public MyFABAnimate(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull FloatingActionButton child,
                                       @NonNull View directTargetChild,
                                       @NonNull View target,
                                       int axes,
                                       int type) {
        super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL;

    }

    /**
     * Xử lý khi RecyclerView cuộn
     * @param target == RecyclerView
     */
    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                               @NonNull FloatingActionButton child,
                               @NonNull View target,
                               int dxConsumed,
                               int dyConsumed,
                               int dxUnconsumed,
                               int dyUnconsumed,
                               int type)
    {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager)((RecyclerView)target).getLayoutManager();
        if (dyConsumed > 0
            && child.getVisibility() == View.VISIBLE
            &&  (linearLayoutManager.findFirstVisibleItemPosition() != 0)) {

            //nếu gọi child.hide(); thì sau đó FAB không còn nhận sự kiện cuộn lên xuống nữa
            //nên cần gọi child.hide(listener);
            child.hide(new FloatingActionButton.OnVisibilityChangedListener() {

                @Override
                public void onHidden(FloatingActionButton fab) {
                    super.onShown(fab);
                    fab.setVisibility(View.INVISIBLE);
                }
            });

        } else if (dyConsumed < 0
                    && child.getVisibility() != View.VISIBLE
                    && (linearLayoutManager.findFirstVisibleItemPosition() == 0)) {
            child.show();
        }
    }

    /**
     * Thiết lập RecyclerView là lớp mà ứng xử của FAB dựa vào để thực hiện
     */
    @Override
    public boolean layoutDependsOn(
            CoordinatorLayout parent,
            FloatingActionButton child,
            View dependency) {
        if (dependency instanceof RecyclerView)
            return true;

        return false;
    }

}

Như vậy bạn đã có lớp net.xuanthulab.floatactionbuttondemo.MyFABAnimate quay lại layout, bạn chỉ việc cho thêm vào FAB thuộc tính: app:layout_behavior="net.xuanthulab.floatactionbuttondemo.MyFABAnimate"

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:src="@drawable/face"
    app:fabSize="normal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_anchor="@id/mylistview"
    app:layout_anchorGravity="bottom|right|end"
    app:layout_behavior="net.xuanthulab.floatactionbuttondemo.MyFABAnimate" />

Chạy thử

FAB trên Header

Trong nhiều thiết kế UI, FAB cũng có thể đặt ở phần đầu Header, ví dụ:

Bạn cũng có thể làm vậy, ở ví dụ trên đơn giản bạn thay cách trình bày Layout, ví dụ:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="match_parent">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="0.7"
            android:scaleType="centerCrop"
            android:src="@drawable/javaforandroid" />


        <android.support.v7.widget.RecyclerView
            android:id="@+id/mylistview"
            android:background="@android:color/holo_orange_light"
            android:layout_width="match_parent"
            android:layout_weight="0.3"
            android:layout_height="match_parent">
        </android.support.v7.widget.RecyclerView>
    </LinearLayout>




    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:src="@drawable/face"
        app:fabSize="normal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/mylistview"
        app:layout_anchorGravity="top|right|end"
        app:layout_behavior="net.xuanthulab.floatactionbuttondemo.MyFABAnimate" />

</android.support.design.widget.CoordinatorLayout>

Chú ý khi dùng FAB - Floating Action Button

  • Khi ẩn / hiện gọi hide()/show để có luốn hiệu ứng thu nhỏ/phóng to
  • Nếu muốn tô màu cho FAB dùng thuộc tính: app:backgroundTint
  • Trên một màn hình chỉ nên có 1 FAB

Đăng ký nhận bài viết mới
Snackbar (Bài trước)
(Bài tiếp) RecylerView