[IOS] Chia sẻ kinh nghiệm làm Segmented Pager giống Pinterest

Ích Ninh văn
Chim cu chăm code
Published in
6 min readMar 29, 2020

Nếu ai đó đã từng code IOS có thể sẽ biết tới thư viện MXSegmentedPager hoặc XLPagerTabStrip. Đây là những thư viện giúp ta có thể dễ dàng chuyển đổi qua lại giữa các ViewController. Nó hiển thị một indicator tương tác giữa các ViewController hiện tại, trước đó, tiếp theo.

Tuy nhiên các thư viện này lại không có được hiệu ứng chuyển view khá là hay ho trong app Pinterest mà tôi từng thấy.
Như ta đã thấy thì khi chuyển qua lại giữa các tab, thì Indicator sẽ di chuyển theo khi scroll. Indicator di chuyển đến đâu sẽ làm highlight text label đến đó.

Sau nửa ngày vật lộn tìm thư viện thay thế, thậm chí tôi đã cố thử tuỳ biến thư viện này bằng cách custom label bằng textEffect AttributedString, tuy nhiên kết quả không được như mong đợi.
Nếu các bạn quan sát kỹ Indicator có bo 4 góc, khi di chuyển Indicator thì text label cũng phải được highlight theo các góc này. Dường như textEffect attributedString không thể giải quyết được bài toàn này. Tiếp tục tìm kiếm tôi đã tìm ra một class gọi là CALayer, class mà bấy lâu nay tôi vẫn dùng nhưng lại không thấy hết được sự vi diệu của nó:(.

Vậy trước hết hãy xem CALayer là gì, nó có đặc tính gì?. Hiểu về CAlayer bạn có thể làm được khá nhiều thứ hay ho đấy.

Nếu bạn muốn làm điều gì đó đặc biệt mà không được cung cấp trong các phương thức của UIView, thì bạn có có thể thực hiện điều đặc biệt đó bằng cách truy cập trực tiếp vào CALayer như: có thể hoán đổi các view hoặc hình ảnh, vẽ những thứ ngoài màn hình, custom animations, vv. Trong bài viết này tôi sẽ giới thiệu với bạn một property có thể làm được animations như đã nêu ở trên. Đó chính là layer mask.

CALayer có một property gọi là mask. Property mask cũng là một CALayer. Nó có tất cả các property như drawing và layout properties của bất kỳ lớp layer nào khác. Mask layer nằm tương đối với cha của nó. Nó được sử dụng theo cách tương tự như một lớp con, nhưng nó không xuất hiện như một lớp con bình thường. Thay vì được vẽ bên trong lớp cha, Mask layer xác định phần của lớp cha có thể nhìn thấy.
Nếu Mask layer nhỏ hơn lớp cha, chỉ các phần của lớp cha giao nhau với Mask sẽ hiển thị. Bất cứ thứ gì bên ngoài Mask sẽ bị ẩn đi.

Lợi dụng đặc tính này chúng ta sẽ dùng Mask layer để làm highlight title label khi Indicator di chuyển.

Cụ thể ở phần cài đặt chúng ta sẽ gồm 2 phần chính:
+ Cài đặt ControlSegment. ControlSegment là một UIControl được custom lại để có animations như trên.
+ Ghép, đồng bộ ControlSegment và PageView. PageView là một class chứa 1 UIPageViewController để quản lý, chuyển đổi qua lại giữa các ViewController.

1. Chi tiết ở ControlSegment chúng ta cần có:

_scrollView: class cha để scroll được khi số lượng label quá nhiều và nó chứa label để hiển thị text mặc định.
selectContentView: chứa class label sẽ được hightlight khi user scroll hoặc select đến.
indicator: Vì k thể set color cho mask layer vì thể chúng ta sẽ dùng indicatorView tạo background cho mask layer.
selectedLabelsMaskView: lớp mặt nạ (Mask) của selectContentView. selectedLabelsMaskView và indicator sẽ có kích thước đúng bằng kích thước của mỗi label mà User select đến.

- Nhìn hình dưới để hiểu rõ hơn

Khi selectedLabelsMaskViewindicator di chuyển, phần frontLabel giao nhau giữa selectedLabelsMaskView và class cha selectContentView sẽ được hiển thị. Còn những frontLabel nằm ngoài mask sẽ bị ẩn đi. Nhờ đó chúng ta sẽ có được hiệu ứng mong muốn.

Đến đây cơ bản là chúng ta đã giải quyết được 80% bài toán đặt ra lúc đầu. Công việc còn lại sẽ là tính toán frame cho từng label tuỳ thuộc vào text của label đó và set frame cho selectedLabelsMaskView tương ứng.

Đầu tiên chúng ta set frame cho scroll, selectContent và set selectContent.layer.mask = selectedLabelsMaskView.layer

Tính width của label dựa vào text của nó.

Có được width chúng ta sẽ tính frame cho từng backLabel và frontLabel. Rồi addSubview backLabel vào scrollView và frontLabel vào selectContent. Dùng array titleLabels để lưu frame của label lại để tiện cho việc quản lý sau này.

Cuối cùng set frame cho indicatorselectedLabelsMaskView

Đến đây chúng ta đã thực hiện xong 99% công việc của ControlSegment. Công việc còn lại là addGestureRecognizer kiểm tra khi có sự kiệnchạm vào các text. Sau đó dựa vào vị trí tap để tính ra index của titleLabels rồi scroll đến vị trí và set frame cho selectedLabelsMaskView tưng ứng.

Như vậy chúng ta đã hoàn thành xong việc dùng layer mask để làm được animations khi chuyển view.

2. Ghép ControlSegment và PageView

Công việc tiếp theo chúng ta sẽ ghép ControlSegment này vào một PageView để có được một Segmented Pager hoàn chỉnh. Phần cài đặt UIPageViewController không có gì khó khăn. Trong khuân khổ bài viết này tôi sẽ chỉ hướng dẫn các bạn đồng bộ scrollView của PageView và scrollView của ControlSegment. Để khi user scroll PageView thì ControlSegment cũng được đồng bộ theo và ngược lại khi click vào tab của ControlSegment thì PageView cũng scroll đến viewcontroller tương ứng.

Ở ControlSegment chúng ta tạo một UIScrollView nữa để lắng nghe contentOffset của UIPageViewController. Vì thế ControlSegment của chúng ta sẽ có 2 ScrollView. một _scrollView chính để hiển thị UI, scrollView còn lại để lắng nghe contentOffset của PageView.

Ở PageView chúng ta gán ScrollView ở trên bằng ScrollView của PageViewController

Implement KVO của scrollView trong ControlSegment. Dựa vào contentOffset ta sẽ tính được progress. Vì func scrollViewDidScroll của UIPageViewController không trả về contentOffset chính xác khi chúng ta scroll. Nên để tính đúng contentOffset ta phải dựa vào currentIndex chính xác của PageView.

Dựa vào progress chúng ta sẽ tính được vị trí và độ rộng của Indicator khi user scroll PageView.

Sau khi đồng bộ được PageView và ControlSegment chúng ta sẽ có kết quả cuối cùng như dưới.:))

Kết

Cảm ơn bạn đã kiên trì đọc đến đây. Hy vọng qua bài này bạn có thể thấy được điều gì đó thú vị từ cách tận dùng thuộc tính mask của CALayer để custom animations.

Có điều gì chưa chính xác hoặc có bất kỳ thắc mắc hay điều gì chưa được đề cập các bạn cứ comment chúng ta cùng thảo luận nhé. Lần nữa xin cảm ơn.

Link demo: https://github.com/ichnv/DemoControlSegment.git

#rikkeisoft

--

--