Chia sẻ kinh nghiệm làm TabLayout giống Pinterest

Nga Nguyen Thien
Chim cu chăm code
Published in
11 min readMar 25, 2020

Xin chào mọi người, hôm trước mình mới được giao một task của myRikkei đó là làm một TabLayout có animation giống như pinterest trên Iphone. Mời mọi người cùng xem qua hình ảnh

Như chúng ta đã thấy thì khi chuyển qua lại giữa các tab, thì phần background ở dưới được bo một chút ở góc và full cả text. Không những vậy, khi di chuyển thì text ở trên background và text ở dưới background khác nhau về màu sắc. Dường như TabLayout của android không support mình vấn đề này rồi. Mình cũng đã tìm thử một số thư viện xem có ai đã dựng TabLayout kiểu này này không nhưng rất tiếc là mình không tìm được. Vì vậy mình đã quyết định tự custom lại một view giống TabLayout được yêu cầu. Do chưa từng làm nên cũng tốn khá nhiều thời gian cũng như cần sự support anh chị em bạn bè thì mình mới làm được. Vì vậy mình muốn viết lại để share cho anh chị em D1 mình. Hy vọng bài viết sẽ giúp ích cho mn. Lần này mình sẽ demo bằng kotlin ạ.

Để làm được thì mình cần sử dụng 2 view

  • 1 view để vẽ text đặt tên là CustomViewTabLayout
  • 1 view khác là HorizontalScrollView để bọc ngoài giúp có thể vuốt scroll đặt tên là CustomMyTabLayout

Các bước thực hiện bao gồm

  • Vẽ text
  • Dựng background nền
  • Dựng background mask
  • Làm animation di chuyển qua lại giữa các text
  • Ghép CustomViewTabLayout vào CustomMyTabLayout
  • Ghép và đồng bộ view dựng được với ViewPager

Trước tiên thì mình cần sơ lược lại một chút lý thuyết

1. Canvas

Canvas được xem như là một bền mặt 2D (hình dung như tờ giấy, bảng) mà chúng ta có thể vẽ bất cứ thứ gì lên đó. Canvas trong Android có cung cấp cho chúng ta các method để vẽ tất cả các đối tượng như sau:

  • Các đối tượng hình học cơ bản (point, line, oval, rect..)
  • Vẽ hình ảnh (bitmap, drawable)
  • Vẽ Path (Tập hợp các điểm)
  • Vẽ Text

2. Paint

Paint trong java dùng để định nghĩa size, color, kiểu nét vẽ mà chúng ta sẽ sử dụng để vẽ bởi canvas (truyền vào method cavas.draw… trong phương thức onDraw của View).

Biến cờ Paint.ANTI_ALIAS_FLAG trong Contructor truyền vào ở trên chỉ định cho Paint rằng vẽ smooth các biên của các đối tượng. Ví dụ như vẽ hình tròn sẽ loại bỏ những hình răng cưa bao quanh hình tròn từ đó có cảm giác hình vẽ lên mượt mà hơn.

Draw Rect

Dùng để vẽ hình chữ nhật

drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)

Vẽ Rect với các thông số cơ bản left, top, right, bottom.

Draw Rect

Dùng để vẽ text thông thường

drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

Vẽ string với vị trí bắt đầu vẽ x, y.

3.Hàm

onMeasure

Đây là một hàm rất quan trọng, trong phần lớn các trường hợp thì đây là nơi để bạn tính toán kích cỡ của view sao cho phù hợp trong layout Trong khi override hàm này, bạn cần gọi hàm setMeasuredDimension(int width, int height) để set giá trị.

onDraw

Ở hàm này bạn sẽ dùng các Canvas và Paint để vẽ ra view của mình. Canvas để bạn có thể vẽ ra các đổi tượng như vẽ hình tròn, chữ nhật, … Paint là để bạn có thể xác định màu sắc, độ trọng suốt, …

View Update

Trong view có 2 hàm để bạn có thể vẽ lại trong khi runtime đó là invalidate() và requestLayout()

  • invalidate(): hàm này được sử dụng để vẽ lại những trường hợp vẽ lại cơ bản, ví dụ như update lại text, màu sắc, hoặc vị trí onTouch, có nghĩa là view chỉ gọi lại hàm onDraw() để cập nhật lại trạng thái
  • requestLayout(): Hàm này khi bạn muốn cập nhật và tính toán lại cả kích cỡ, vị trí của view và sau đó vẽ lại view đó theo kích cỡ mới.

Bắt tay vào làm thôi

Đầu tiên chúng ta sẽ vẽ các text

Tưởng tượng bề mặt vẽ của chúng ta là bề mặt 2D có trục tọa độ Oxy trong đó Ox là trục hoành (trục ngang), Oy là trục tung (trục dọc). Ta sẽ thử vẽ 1 text “Dành cho bạn” trước nhé.

Khi vẽ text chúng ta cần 1 biến textPaint là Paint để vẽ text và 1 biến sourceImage là một Bitmap hiển thị chứa text của mình.

Giải thích:

các biến width là độ rộng của bitmap (fix trước 300dp xong mình sẽ tính sau)

biến height là chiều cao của bitmap

Trở lại với tọa độ Oxy thì

Text mình đang vẽ ở tọa độ Ox = 0 và Oy = 50 (bằng chiều cao của bitmap).

Bimap mình đang vẽ ở tọa độ Ox = 0 và Oy = 0

Nhưng text của mình cần lại là một list các text bởi vậy chúng ta sẽ sửa code một chút như sau

Giải thích: Mình sẽ thêm một số biến như sau

textPading : Padding giữa mỗi text (Mình cho thêm vào để giữa các text sẽ có khoảng cách, không bị dính vào nhau)

titles: là list các text mình cần vẽ

titlews : là list các độ rộng của text mình vẽ để mình tính lại độ rộng của bitmap. Công thức

titleps: là list các tọa độ vị trí của text công thức:

Khi vẽ mình sẽ mình sẽ thay đổi tọa độ tương ứng của text ở trục Ox ở dòng code

srcCanvas.drawText(titles[i], posX, 54f, textPaint)

set lại size cho text to lên một chút nhé (đổi từ 14sp ->16sp)

Đây là thành quả sau khi thay đổi

Tiếp theo mình cần vẽ background màu đen nhé

Để có một text màu trắng đè lên trên mình sẽ sử dụng 2 bitmap có độ rộng và chiều cao bằng nhau, trong đó 1 bitmap là bitmap vẽ background màu đen của mình và 1 bitmap màu trắng (màu mình cần đổi màu text khi vẽ trên nền đen).

hình ảnh minh họa

trước tiên mình sẽ vẽ background thứ nhất (background nền đen trước) với độ rộng tạm thời là độ rộng của text thứ 1 trong list và chiều cao bằng 50dp, có padding 2 bên là 4dp mình sẽ sử dụng một cọ vẽ có tên là bgPaint

Khởi tạo cọ vẽ

chỉnh sửa lại tọa độ vẽ height của list text như sau:

tọa độ y của text = ½ độ rộng của background + ½ độ rộng của text size

(hiện text size của mình đang là 16sp thì mình sẽ lấy ½ của 16 là 8dp)

thay lại tọa độ vẽ trước đó của text nhé

Tiếp tục mình sẽ vẽ tới background

Đây là kết quả sau khi vẽ xong

Cuối cùng mình sẽ vẽ 1 bitmap màu trắng (màu mình cần đổi màu text khi vẽ trên nền đen) có các chỉ số bằng đúng bitmap màu đen đặt tên là bitmapMask với cọ sử dụng là xferPaint

mình sẽ sử dụng mode vẽ SRC_IN có tác dụng giữ các pixel nguồn và đích bao phủ loại bỏ các pixel còn lại của nguồn và đích ( trong trường hợp này nguồn và đích chính là text và bitmap được dựng lên) sẽ được màu mới của text và loại bỏ màu nền của background (xem lại hình minh họa ở trên để có thể hiểu hơn)

Và đây là kết quả mình nhận được

Sau khi có được background ưng ý mình sẽ làm animation di chuyển qua lại giữa các tab nhé.

Chúng ta sẽ giả lập click của người dùng bằng cách sử dụng hàm onTouchEvent kiểm tra khi có sự chạm vào các text để làm được điều này mình sẽ tách phần code ở trên ra thành một số fun để sử dụng lại và sửa lại một chút tính toán để phần background của mình

và gọi hàm processMask() trong onMeasure

Giải thích code

Hàm getWidthOf dùng để get độ rộng cho back ground của text vị trí đó

Hàm getPosXOf dùng để get vị trí tọa độ Ox tiếp theo.

Hàm updatePosition dùng để set vị trí hiện tại của background

Hàm processMask dùng để tính toán vị trí mới cho background

Trong hàm processMask chúng ta sẽ tính toán lại backgroud mỗi khi nó di chuyển bằng ccaách sử dụng toán tử MathUtils.lerp để tính toán lại background và tọa độ X khi di chuyển

Toán tử lerp dùng để thực hiện nội suy giữa hai vector và trả về một vector mới tương ứng với thành phần t trong hàm.

Giả sử ví dụ: a = 1, b = 2 amount = 0.5 thì kết quả là 1.5

(ví dụ 0.5 thì giá trị nó ở giữa, 0.7 là lệch bên phải, 0.4 là lệch bên trái)

Như vậy khi đi từ trái sang phải, tọa độ X, và background sẽ di chuyển dần từ trái qua phải và ngược lại.

Tiếp tục chúng ta sẽ giả lập click của người dùng bằng cách sử dụng hàm onTouchEvent kiểm tra khi có sự chạm vào các text

Sử dụng animation để tạo sự di chuyển từ vị trí hiện tại đến vị trí cần di chuyển.

Trong hàm onTouchEvent sẽ check tọa độ ex và kiểm tra xem tọa độ ex nằm trong khoảng của vị trí nào để di chuyển đến.

Và đây là kết quả mình nhận được

Nhưng đây là view có 2 item, nếu view có nhiều hơn vậy, thì text của chúng ta sẽ bị che mất, vì vậy mình sẽ tiếp tục cho view của mình đã có vào một HorizontalScrollView đặt tên là CustomMyTabLayout để có thể vuốt được qua lại. Như vậy text của mình sẽ được truyền từ bên CustomMyTabLayout sang View vừa dựng của mình.

Đây là kết quả của mình

Nhưng có vẻ chữ làm đẹp đang không scroll ra giữa vị trí của screen

để có thêm điều này mình sẽ thêm 1 chút code trong phần processMask

để có được thì trước tiên ta cần lấy độ rộng của màn hình hiện tại

và thêm phần tính toán ở hàm processMask

Giải thích thêm một chút, chúng ta sẽ tính toán vị trí start trên màn hình và vị trí cuối cùng muốn di chuyển tới trên màn hình. trong đó

sScrollPosition là tâm toạ độ x của title 1.

eScrollPosition là tâm toạ độ x của title 2

và đây là kết quả

Có một bug phát sinh đó là khi làm như thế này thì nếu người dùng vuốt chỉ để muốn show tới view đằng sau thì sẽ lại bị hiểu nhầm thành là click

vì vậy mình sẽ check thêm isClickable = true và isFocusable = true

và sửa lại phần check như sau mỗi lần có action down sẽ lưu lại vị trí click, nếu có action up mà vị trí action up trùng với action down hoặc lệch một tọa độ nhỏ khoảng 15 (con số này có thể thay đổi để tăng độ nhạy của việc check click) cũng như thêm phần trường hợp tab liên tục vào màn hình

Mình sẽ đồng bộ tab được click thì viewpager được scroll tới vị trí đó bằng cách trả call back trong hàm onTouchEvent

Ngoài ra thì ở trên khi vuốt qua lại giữa các viewpager thì mình sẽ cập nhật vị trí mới cho tab ở bên trên bằng cách sử dụng hàm updatePosition truyền vị trí mới cho tab.

Và đây là kết quả cuối cùng của chúng ta

Vì lý do thời gian và kinh nghiệm có hạn nên khi viết không thể tránh khỏi sai sót mong mọi người thông cảm và góp ý để bài viết tốt hơn. Chúc mọi người một ngày làm việc vui vẻ. Cảm ơn mọi người.

Tài liệu tham khảo

https://developer.android.com/reference/android/graphics/PorterDuff.Mode

Link demo

https://github.com/ngant97/DemoCustomTabLayout

--

--