Tùy chỉnh tiêu đề xem bảng hoạt ảnh giống ứng dụng Tiki

Tram Ho

Introduction

Thỉnh thoảng khi sử dụng các app nổi tiếng, chúng ta có thể thấy có một số màn hình có phần header được animate thu nhỏ, phóng to, thay đổi cấu trúc UI mỗi khi user scroll lên hoặc xuống.

Ví dụ như màn hình profile của app Twitter:

Hoặc trang chủ của app Tiki:

Hoặc UI ấn tượng của app Uber Eats:

Việc customize animate header như vậy sẽ làm cho UX của app mới mẻ và bớt nhàm chán, đơn điệu. Khi ở top, user có nhìn thấy được toàn bộ phần header, còn khi scroll xuống thì header co lại, app sẽ hiển thị được nhiều thông tin hơn.

Trong bài viết này, chúng ta sẽ cùng thử bắt chước làm animation cho header gần giống app Tiki trên. Header sẽ expand hoặc collapse tùy thuộc vào table view đang được scroll lên hay xuống.

Getting started

Để bắt đầu, chúng ta cần tạo một UIViewController trong đó chứa một UIView và một UITableView. Hãy đặt tên cho UIView vừa tạo là Header View và constraint nó với top, left, right của UIViewController, còn height constraint thì set bằng 88. Tương tự, UITableView thì set constraint bottom, left, right với UIViewController và top constraint bằng bottom constraint của Header View. Cụ thể như sau:

Để thay đổi height của header view, chúng ta cần tạo IBOutlet cho height constaint của Header View. Để implement data cho table view, cần tạo thêm IBOutlet cho cả table view nữa.

Việc dummy data cho table view được thực hiện đơn giản như sau:

Defining min and max values

Giống các app ví dụ kể trên, khi mới vào các màn hình này, ban đầu header sẽ được hiển thị với max height. Ở trạng thái này, header sẽ có đầy đủ các thành phần UI cần thiết. Khi scoll xuống dưới, các UI như title, image, button sẽ được tự động sắp xếp lại, co lại để tiết kiệm tối đa không gian, lấy chỗ để hiển thị được nhiều content bên dưới hơn.

Để làm được animation này, chúng ta cần khai báo các maximum value và minimum value thể hiện height của header khi được expand hay collapse tối đa. Hãy thêm các class properties sau:

Ở trạng thái collapse tối đa, header sẽ có height bằng một nửa so với trạng thái expand tối đa ban đầu.

Để đảm bảo header luôn được expand tối đa khi view controller được hiển thị, rất đơn giản, trong methods viewWillAppear chỉ cần set lại headerHeightConstraint bằng maxHeaderHeight:

Bây giờ thì đã implement được trạng thái khởi tạo expand tối đa của header. Tiếp theo đến phần animate header khi scroll.

Scrolling up and down

Protocol UIScrollViewDelegate (tự động conform trong protocol UITableViewDelegate) có rất nhiều method hữu dụng giúp chúng ta theo dõi, xử lý việc scroll của scroll view. Method đầu tiên mà chúng ta sẽ sử dụng ở đây đó là scrollViewDidScroll(scrollView: UIScrollView). Method này được gọi mỗi khi scroll position của table view (scroll view) bị thay đổi. Có thể sử dụng property contentOffset.y của scroll view để lấy được scroll position hiện tại. Từ đó resize, animate header view.

Để xác định xem scroll view đang scroll lên hay xuống, một cách đơn giản là sử dụng một class property nữa (ví dụ previousScrollOffset). Sau đó lấy scroll position hiện tại trừ đi scroll position trước đó, nếu nhỏ hơn 0 thì có nghĩa là scroll view đang scroll lên, còn nếu nhỏ hơn 0 thì đang scroll xuống.

Giả sử property previousScrollOffset được set đúng value thì logic trên được cụ thể hóa thành code như sau:

Để set đúng value cho previousScrollOffset, chỉ cần set nó bằng với scroll position hiện tại ở cuối method:

Sau khi xác định được scroll direction, chúng ta có thể tính toán và thay đổi height của header. Header sẽ collapse theo tỉ lệ mà scroll view offset thay đổi nên có thể sử dụng biến scrollDiff ở trên để tính toán height cần thay đổi. Tuy nhiên, header lại không được expand hay collapse ngoài quá max, min height đã khai báo ở trên. Vì vậy, chúng ta cần sử dụng các hàm max, min, abs để tính toán và giới hạn lại giá trị height:

Trong đoạn code trên, biến newHeight được dùng để xác định new height cho header dựa trên scroll direction. Giá trị new height này sẽ được apply vào headerHeightConstraint nếu nó khác với constant hiện tại.

Nếu run thử project ngay lúc này, chúng ta có thể thấy có một số behavior dị thường xuất hiện mỗi khi scroll để top hoặc bottom của scroll view. Điều này là vì animation bounce mặc định của scroll view mỗi khi scroll view được kéo ngoài giới hạn content size của nó. Vì vậy đoạn logic xác định scroll direction lên hay xuống ở trên bị sai khi scroll view bị bounce như vậy.

Để fix bug này chỉ cần thêm một số logic check như sau:

Với việc update scroll direction như trên, run lại project, chúng ta sẽ thấy header expand và collapse mượt mà mỗi khi scroll đến top hoặc bottom của scroll view.

Trong một số trường hợp, khi mà table view chỉ có một vài cell và content size của scroll view nhỏ hơn height của screen, chúng ta sẽ không cần collapse header. Vì trong trường hợp này, tính cả header và content của table view, vẫn còn đủ không gian trong màn hình. Vì vậy, để ngăn header collapse trong trường hợp ngoại lệ này, chúng ta cần check xem có còn không gian để scroll không ngay cả khi header bị collapse lại. Thêm đoạn code check đơn giản sau trước đoạn logic tính toán new height để fix case này:

Method canAnimateHeader(_ scrollView: UIScrollView) -> Bool:

Stop scrolling while expanding/collapsing

Một điều cần làm nữa để đảm bảo việc animate header diễn ra mượt mà đó là chúng ta phải đóng băng việc scroll của scroll view trong lúc header đang expand/collapse. Vì trong đoạn code trên, chúng ta đã có câu lệnh if để check xem khi new height khác với giá trị constant của headerHeightConstraint hiện tại nên có thể dựa vào đó để biết khi nào thì header đang được animate.

Method setScrollPosition(_ position: CGFloat):

Run project và kiểm chứng, mỗi khi header đang được expand/collapse thì table view của chúng ta không còn bị scroll theo và bị header che mất nữa.

Snap header fully expanded/collapsed

Behavior này giúp header không bị lơ lửng ở giữa 2 state fully expanded hoặc fully collapsed. Việc implement animation trong scrollViewDidScroll là không đủ. Mà UIScrollViewDelegate cũng không có delegate method nào là scrollViewDidStop() nên chúng ta cần kết hợp 2 method có sẵn để xác định khi nào thì kết thúc scroll.

Như cái tên của method, scrollViewDidEndDecelerating() cho chúng ta biết khi nào thì scroll view dừng scroll sau quá trình “di chuyển” và “giảm tốc”. Còn scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) thì cho biết khi nào scroll view dừng scroll sau khi user nhấc ngón tay lên. Biến boolean decelerate bằng true nếu scroll view tiếp tục di chuyển sau đó. Còn bằng false nghĩa là scroll chuẩn bị dừng lại.

Sau khi xác định được thời điểm scroll view dừng scroll, chúng ta cần tạo một helper method để implement logic để animate header expand hay collapse, không có trạng thái lơ lửng ở giữa. Header height sẽ chỉ có 2 state max hoặc min. Để làm được điều này cần tính được một điểm mid point. Nếu height của header lớn hơn điểm mid point thì sẽ expand hoàn toàn header. Và ngược lại sẽ collapse hoàn toàn header.

Nếu run app ngay, header đã không còn trạng thái lơ lửng giữa min/max height khiừng scroll giữa chừng nữa. Tuy nhiên việc expand/collapse header khi header vượt quá mid point không có animation nên nhìn rất giật cục.

Thêm và thay thế animation với đoạn code sau:

Animating elements within the header

Trong phần này, chúng ta sẽ thực hiện animate, thay đổi vị trí, cấu trúc, cách sắp xếp các thành phần UI trong header view.

Trong storyboard, embed Header View vào một UIView mới, đặt tên là Header View Container. Set top constraint của Header View Container bằng top constraint của super view (không phải với safe area), left, right với safe area, bottom với top của UITableView. Set màu cho container view trùng màu với header view đã có. Mục đích là để cover và fill cùng màu lên status bar.

Constraint của header view thì update lại: top với top của view controller, left, right, bottom với left, right, bottom của container.

Tiếp theo thêm các UIImageView, UIButtonUITextField và constraint như sắp xếp sau:

Khi collapse header view, chúng ta sẽ làm mờ dần image view và thay đổi constant trailing constraint của text field. Vì vậy cần tạo IBOutlet cho chúng trong view controller. Các UI element còn lại như 2 button thì vị trí cố định ở top right, không thay đổi. Text field sẽ co lại những vẫn giữ nguyên ở vị trí bottom left.

Ngoài ra để tính toán được khoảng co lại của text field sao cho bằng với min x của button chat, chúng ta vẫn cần tạo IBOutlet cho chat button này.

Method updateHeader() để thực hiện animate UI element trong header như sau:

Cuối cùng, gọi method updateHeader() này trong viewWillAppear(), collapseHeader(), expandHeader() và cả scrollViewDidScroll():

Kết quả:

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo