Month: February 2020

  • Binding library android -Xamarin Android

    Kết quả hình ảnh cho binding android xamarin

    Với một vài dự án khách hàng đã code sẵn cho 1 cái library android rồi, và họ sẽ gửi cho bạn sử dụng cái lib đó. Hoặc là bạn có 1 cái lib rất ngon trên android. Nhưng muốn dùng trong project Xamarin của bạn mà không muốn code lại logic của nó. Như vậy bạn cần binding nó vào project của bạn và chỉ việc dùng không cần code lại 1 tí logic nào.

    I. Input:
    Project dự án code bằng ngôn ngữ Xamarin
    Các file android libraries .aar (Android Archive Library)
    Bản chất của file này là file .zip gồm
    – Compiled Java code
    – Resource Ids (File R)
    – Resources
    – Meta-data (for example, Activity declarations, permissions)

    II. Output
    Project Xamarin có thể gọi và sử dụng được class chứa trong file .aar

    III.Guideline
    1. Tạo 1 project binding với 1 file .aar cơ bản
    1) Add new project Binding library(Android)
    2) Add file .aar vào folder Jar
    3) Set build action cho .AAR file là LibraryProjectZip
    4) Chọn target framework cho project lib nếu cần
    5) Build Bindings Library.
    Sau khi tạo xong và build thành công chúng ta sẽ add file đó vào project Xamarin của chúng ta muốn sử dụng.
    Vậy là chúng ta có thể call tới class chứa trong file .aar.
    Mình chỉ nói các bước thực hiện thôi, chi tiết các bạn nên tham khảo tại : https://docs.microsoft.com/en-us/xamarin/android/platform/binding-java-library/binding-an-aar

    Như ở trên là cách tạo và add 1 project binding library với file source code trong file aar là đơn giản.
    Với những file .aar phức tạp và chúng có link đến nhau thì sao ?

    2. Tạo project binding nâng cao
    1. Đầu tiên các bạn vẫn phải thực hiện các bước như trên. Vì xamarin có 1 nhược điểm là 1 file aar bắt buộc bạn cần phải tạo ra 1 project binding
    2. Cần phải xác định file aar nào sử dụng file aar nào.
    Ví dụ chúng ta có 2 file aar, file thứ nhất call đến 1 class trong file thứ 2
    Như vậy làm thế nào để biết được file nào dùng đến file nào
    – Bạn cần 1 tool để decode java trong file aar (Android studio chẳng hạn)
    – Sau đó bạn mở những class trong file aar ra. Xem phần import của chúng sử dụng những class nào có chứa trong file aar khác không
    – Đôi khi trong file aar nó sẽ sử dụng dependence đến 1 lib nào mà k chứa trong tất cả file aar của bạn. Thì bạn cần xem nó là những lib nào.
    Click chuột phải vào Packages của Project đó chọn Add packages. Rồi tìm xem tên lib đó có trong nuget không.
    Nếu không có, thì bạn có thể tìm và tải file jar của lib ấy về rồi copy vào folder Jar. Build action bạn để là EmbeddedJar
    Bạn thử build xem còn lỗi gì không.
    Nếu còn lỗi. Bạn cần phải customizing binding.
    3.Customizing Binding
    Với android chúng ta chỉ cần customize lại file Metadata.xml Transform File.
    Trong file Metadata.xml thì sẽ có 3 element :
    – add-node
    – attr
    – remove-node

    Mình cũng làm 1 vài project liên quan đến binding library. Và cũng customize file metadata.xml này, mình thấy như sau

    khi thấy error bạn chọn vào 1 item error

    Bạn có thể nhìn thấy dòng // Metadata.xml Xpath classs ….
    hãy coppy từ chỗ path=”…”
    Bạn có thể sử dụng remove-node trong file metadata.xml với lỗi ”do not exist in the namespace …’
    Thực ra bạn remove node này nó không ảnh hưởng gì đến code trong project cả. Việc generate class java sang c# ở đây chỉ là Android.Runtime.Register thôi.
    chứ không phải là nó generate all source code của cả class java sang c# .

    Một số trường hợp bạn không nhìn thấy class mình dùng được generate ra
    Trường hợp này chúng ta sẽ add bằng tay dùng add-node
    Ví dụ :

    <add-node path="/api/package[@name='your package name']">	
            <class abstract="false" deprecated="not deprecated" extends="java.lang.Object" 	
                extends-generic-aware="java.lang.Object" final="false" name=" your class name" static="true" visibility="public">	
            </class>	
    </add-node>	

    Một số trường hợp khác như tên classs trùng nhau, rồi đổi visibility của class … thì bạn dùng thằng att để thay đổi attribute của nó.
    Về chi tiết các bạn có thể tham khảo tại : https://docs.microsoft.com/en-us/xamarin/android/platform/binding-java-library/customizing-bindings/java-bindings-metadata
    Một vài lưu ý nữa là kể cả việc file aar bị mã hoá cũng không ảnh hưởng gì đến việc binding nhé. Vì những class a, b, c trong đó bạn trả bao giờ dùng đến cả.

    Bài viết của mình xin hết, nếu bạn nào dùng mà cảm thấy không hiểu thì contact với mình.

    Nếu các bạn cần binding với xamarin ios xem tại: https://magz.techover.io/2019/12/30/binding-objective-c-libraries-xamarin-ios/

    Thanks you!

  • Sự khác nhau giữa Authentication và Authorization

    Sự khác nhau giữa Authentication và Authorization

    1. Authentication

    Authentication là xác thực, chỉ quá trình định danh một tài khoản đang vào hệ thống chính là người đó chứ không phải ai khác. Đây là bước ban đầu của mọi hệ thống có yếu tố người dùng. Nếu không có bước xác thực này, hệ thống của bạn sẽ không biết được người đang truy cập vào hệ thống là ai để có các phản hồi phù hợp, thường biểu hiện ở hình thức đơn giản nhất chính là form đăng nhập vào hệ thống. Đây là mô hình xác thực dựa trên yếu tố “mật khẩu”. Mật khẩu là một trong những phương pháp được triển khai cho hệ thống xác thực (authentication). Một số phương thức xác thực thông dụng khác là khóa (public & private), sinh học (vân tay, tròng mắt, khuôn mặt)…

    2. Authorization

    Sau khi xác định được “danh tính” của tài khoản thì hệ thống mới chỉ trả lời được câu hỏi “Đó là ai?”, chúng ta sẽ tiến hành một bước quan trọng không kém đó là trả lời câu hỏi “Người đó có thể làm được gì?”, hay xác định quyền (phân quyền) của tài khoản hiện tại vừa mới được xác thực.
    Hệ thống của bạn có thể sẽ rất phức tạp bởi nhiều tính năng quan trọng và mạng lưới phòng ban dày đặc và cần phân chia quyền sử dụng rõ ràng nên việc thiết kế một hệ thống phân quyền cho từng thành viên là một việc làm cực kỳ quan trọng và cần thiết.

    Các hình thức phân quyền thường gặp là:

    + Role-based authorization: Phân quyền dựa trên vai trò của người dùng. Ví dụ trong WordPress có các role như là  Subscriber, Contributor, Author, Editor, Administrator và mỗi một role sẽ có những quyền khác nhau và mỗi người dùng sẽ được phân role có quyền tương ứng. Đối với những hệ thống có nhiều người dùng thì role-based là cách tiếp cận tốt nhất để tiết kiệm thời gian trong việc phân quyền.

    + Object-based authorization: Phân quyền theo đối tượng. Cách này sẽ phân quyền cho từng đối tượng cụ thể. Ví dụ những đối tượng trong nhóm A, B được phân quyền chỉnh sửa các bài viết trong danh mục. Nhưng đối tượng trong nhóm A chỉ chỉnh sửa được bài viết trong danh mục C, đối trượng trong nhóm B chỉ chỉnh sửa bài viết trong danh mục D.

    Nguồn tham khảo thêm:

    http://searchsecurity.techtarget.com/definition/authentication


  • iOS/Auto Layout – Phần 1: Auto layout là gì?

    iOS/Auto Layout – Phần 1: Auto layout là gì?

    Lời mở đầu

    Để có thể tạo ra các ứng dụng iOS thì việc sử dụng auto layout là không thể tránh khỏi. Đối với các Developer đã có kinh nghiệm thì có vẻ không khó khăn là bao. Nhưng đối với các iOS Developer mới họ thường khá lúng túng với công việc này. Vì vậy loạt bài viết lần này mình sẽ chia sẻ về Auto layout để các bạn có thể cảm thấy tự tin hơn khi sử dụng Auto Layout 😀



    Để sử dụng Auto Layout thì đầu tiên chúng ta phải hiểu nó là cái gì và sử dụng để làm gì. Vì vậy phần 1 mình sẽ nói về định nghĩa của Auto layout cho các bạn mới làm về iOS giúp các bạn hiều và dần làm quen với nó.
    OK, Chúng ta bắt đầu thôi :v

    Auto layout là gì?

    Auto layout là việc tự động tính toán kích thước và vị trí của tất cả các “view” trong view hierarchy của bạn, dựa trên các ràng buộc được đặt trên các “view” đó.

    Ví dụ: Bạn có thể giới hạn một Button sao cho Button nằm ở giữa theo chiều ngang với ImageView và sao cho cạnh trên của Button luôn cách 8 “points” so với cạnh dưới ImageView. Lúc này, nếu kích thước hoặc vị trí của ImageView thay đổi thì vị trí của Button sẽ tự động điều chỉnh cho phù hơp. (Hình 1)

    Cách tiếp cận dựa trên ràng buộc này để thiết kế cho phép bạn xây dựng giao diện người dùng tự động đáp ứng với cả những thay đổi bởi các tác nhân bên trong và bên ngoài(Internal and external changes).

    Hình 1

    NOTE:
    • “view”: Ở đây được hiểu là tập hợp các UI để hiển thị trên màn hình như: UILabel, UIButton, UIView .v.v.
    • “points”: Là đơn vị đo lường sử dụng trong thiết kế UI của iOS.

    Những tác nhân thay đổi view từ bên ngoài (External Changes)

    Nó xảy ra khi kích thước hoặc hình dạng của supperview thay đổi. Với mỗi thay đổi, bạn sẽ phải cập nhật lại layout của view hierarchy để sử dụng không gian có sẵn một cách tốt nhất. Dưới đây là một số tác nhân thay đổi bên ngoài phổ biến:
    • Người dùng thay đổi kích thước cửa sổ (window) trong OS X.
    • Người dùng truy cập hoặc rời khỏi Split View trên iPad.
    • Người dùng sử dụng tính năng xoay màn hình (Rotates) (iOS).
    • Khi các thanh cuộc gọi hoặc ghi âm xuất hiện hoặc biến mất (iOS).
    • Khi bạn muốn hỗ trợ các size class khác nhau.
    • Khi bạn muốn hỗ trợ các kích thước màn hình khác nhau

    Hầy hết những tác nhân thay đổi này có thể xảy ra trong thời gian chạy của ứng dụng và chúng yêu cầu phản hồi linh hoạt từ ứng dụng của bạn. Ngoài ra, như là việc hỗ trợ cho các màn hình với kích thước khác nhau, ứng dụng cũng cần thích nghi với các môi trường khác nhau. Mặc dù kích thước màn hình thường không thay đổi lúc runtime, nhưng việc tạo một giao diện người dùng dễ dàng thích thích nghi(adaptive) khiến cho ứng dụng của các bạn chạy tốt trên cả iPhone 4S, iPhone 6 Plus, iPhone X thậm chí là cả iPad. Auto Layout cũng là phần chủ đạo cho việc hỗ trợ Slide Over và Split view trên iPad.

    Những tác nhân thay đổi view từ bên trong (Internal Changes)

    Nó xảy ra khi kích thước của các view hoặc các controls(Thanh điều khiển) trong giao diện người dùng của bạn thay đổi.

    Dưới đây là một vài nguyên nhân phổ biến:

    • Nội dung bị thay đổi bởi những thay đổi từ ứng dụng.
    • Ứng dụng hỗ trợ đa quốc gia
    • Ứng dụng hỗ trợ Dynamic type (iOS): Cho phép thay đổi style của ứng dụng (font, fontsize, …)

    Khi mà mội dung của ứng dụng thay đổi, nội dung mới có thể yêu cầu một layout khác với layout hiện tại. Điều này thường xảy ra trong ứng dụng khi nó hiển thị text hoặc image(hình ảnh). Ví dụ, một ứng dụng tin tức cần phải điều chỉnh layout của nó dựa trên kích thước của từng loại tin tức. Tương tự, một ứng dụng ghép ảnh phải xử lí một loạt các kích thước của hình ảnh và tỉ lệ khung hình(aspect ratio).

    Việc làm ứng dụng hỗ trợ đa quốc gia là quá trình tạo ra một ứng dụng có thể hiển thị tốt ở trên các ngôn ngữ, khu vực và văn hóa khác nhau. Bố cục của ứng dụng hỗ trợ đa quốc gia phải tính đến những khác biệt này và cần hiển thị chính xác trong tất cả các ngôn ngữ và khu vực mà ứng dụng hỗ trợ.

    Ứng dụng hỗ trợ đa quốc gia có 3 tác dụng chính trên bố cục. Đầu tiên khi bạn chuyển giao diện sang một ngôn ngữ khác thì các Label sẽ yêu cầu phải có một khoảng trống khác. Ví dụ như: Tiếng đức thì cần nhiều không gian hơn tiếng anh. Tiếng nhật thì lại cần ít khoảng trống hơn nhiều.

    Thứ hai, Đinh dạng bạn sử dụng để hiển thị ngày và số có thể thay đổi giữa các khu vực, ngay cả khi ngôn ngữ không bị thay đổi. Mặc dù những thay đổi này thường không to bằng thay đổi ngôn ngữ, nhưng giao diện người dùng vẫn cần thích ứng với những thay đổi đó.

    Thứ ba, thay đổi ngôn ngữ có thể ảnh hưởng không chỉ đến kích thước của văn bản, mà cả tổ chức bố cục của nó. Các ngôn ngữ khác nhau sẽ sử dụng các hướng bố trí khác nhau. Ví dụ như: Tiếng Anh thì bố trí từ trái qua phải, còn tiếng Ả Rập và Do Thái thì ngược lại. Nói chung, thứ tự của các phần trong giao diện người dùng phải phù hợp với hướng bố trí bố cục trong ngôn ngữ đó. Nếu một Button nằm ở góc dưới bên phải của màn hình khi sử dụng Tiếng Anh thì nó nên được được nằm ở góc dưới bên trái khi sử dụng tiếng Ả Rập.

    Cuối cùng, nếu ứng dụng iOS của bạn hỗ trợ kiểu Dynamic type, người dùng có thể thay đổi kích thước, phông chữ được sử dụng trong ứng dụng của bạn. Điều này có thể thay đổi cả chiều cao lẫn chiều rộng của bất kỳ text trong giao diện người dùng của bạn. Nếu người dùng thay đổi kích thước phông chữ(font) của bạn khi ứng dụng đang chạy thì cả phông chữ (font) và bố cục(layout) đều phải thích ứng với thay đổi đó.

    Tổng kết

    Auto Layout là việc định nghĩa tập hợp các ràng buộc. Các ràng buộc này đại diện cho quan hệ giữa các view với nhau. Auto layout sau đó sẽ tính toán kích thước và vị trí của các view dựa trên các ràng buộc của chúng. Điều này tạo ra các bố cục tự đáp ứng được cả với thay đổi bên trong và bên ngoài.
    Để sử dụng auto layout chúng ta cần phải hiểu về logic đằng sau các layouts dựa trên các ràng buộc của nó.



    Cảm ơn các bạn đã theo dõi bài viết 😀

  • Operation (P3)

    Operation (P3)

    Đến với phần cuối của loạt bài viết về Operation nhưng cũng không kém phần quan trọng, mình sẽ nói về Async Operation và Cancel Operation.

    Nội dung bài viết:

    • Cancel Operation
    • Async Operation
    • Demo

    Cancel Operation

    • Cách dùng khá đơn giản, chỉ cần gọi cancel() để cancel 1 operation. Tuy nhiên, 1 operation chỉ có thể bị cancel trước khi nó được bắt đầu được thực hiện.
    • Bản chất của việc gọi cancel là sẽ set state của operation thành isCancelled = true.
    operation.cancel()

    Cancel toàn bộ operation:

    Để cancel toàn bộ các operations trong 1 operation queue, chỉ cần gọi:

    operationQueue.cancelAllOperations()

    Note: Chỉ cancel những task chưa được thực hiện

    Vậy làm thế nào có thể cancel 1 operation đang thực hiện? Câu trả lời là sẽ kiểm tra state isCancelled trong thân hàm.
    Hãy xem ví dụ ở cuối bài để hiểu thêm.

    Async operation

    1 Operation nếu được khởi tạo default thì sẽ hoạt động theo kiểu synchronous. 1 vòng đời của operation khi đó theo các state là: isReady -> isExecuting -> isFinished.

    Nhưng nếu bạn muốn các operation của bạn hoạt động theo kiểu asynchoronous bởi diều đó chắc chắn sẽ khiến app của bạn họat động nhanh hơn?
    Điều đó là hoàn toàn có thể, tuy nhiên nếu operation hoạt động kiểu async, nó sẽ trả về quyền điều khiển ngay lập tức (xem lại bài GCD part 1). Vì vậy, state của operation đó sẽ trở thành isFinished ngay lập tức. Do đó, bạn sẽ phải làm thêm 1 vài việc để custom lại state của operation nếu bạn muốn operation đó chạy theo kiểu async.

    Cách làm ở đây về cơ bản là sẽ viết 1 async operation subclass để quản lí state, và giao tiếp với lớp cha Operation của nó thông qua KVO.
    Nghe có vẻ khá dài, nhưng đừng lo lắng, chỉ cần làm 1 lần thôi, lần sau bạn sẽ chỉ cần gọi và dùng.

    class AsyncOperation: Operation {
        // State enumaration
        enum State: String {
            case ready, excuting, finished
    
            // 1
            fileprivate var keyPath: String {
                return "is" + rawValue.capitalized
            }
        }
        // State property
        // 2
        var state: State = .ready {
            // 3
            willSet {
                debugPrint("New value \(newValue)")
                willChangeValue(forKey: newValue.keyPath)
                debugPrint("Will change value 1 for key \(newValue.keyPath)")
                willChangeValue(forKey: state.keyPath)
                debugPrint("Will change value 2 for key \(state.keyPath)")
            }
            didSet {
                debugPrint("old value \(oldValue)")
                didChangeValue(forKey: oldValue.keyPath)
                debugPrint("Did change value 1 for key \(oldValue.keyPath)")
                didChangeValue(forKey: state.keyPath)
                debugPrint("Did change value 2 for key \(state.keyPath)")
            }
        }
    }
    1. State của operation default sẽ là ready
    2. keyPath là 1 computed property sẽ giúp bạn lấy state hiện tại của operation.
    3. Bởi vì bạn cần gửi các notification khi bạn thay đổi state, bạn sẽ dùng didSet và willSet để hứng notification. -> Trong nhiều trường hợp có thể bạn sẽ không cần dùng đến, nhưng nên khai báo để dễ dàng quan sát, debug.

    Base Properties

    extension AsyncOperation {
        // 1
        open override var isReady: Bool {
            return super.isReady && state == .ready
        }
    
        open override var isExecuting: Bool {
            return state == .excuting
        }
    
    
        open override var isFinished: Bool {
            return state == .finished
        }
        // 2
        open override var isAsynchronous: Bool {
            return true
        }
        // 3
        open override func start() {
            // 4
            if isCancelled {
                state = .finished
                return
            }
    
            main()
        }
    
        open override func cancel() {
            super.cancel()
            state = .finished
        }
    }
    1. override lại các state vì bạn muốn tự quản lí state cho riêng mình.
    2. set thuộc tính isAsynchronous thành true để operation chạy async.
    3. override lại func start(). Khi đưa 1 operation vào queue, queue sẽ gọi start để thực hiện chạy 1 operation.
    4. Kiểm tra trước khi thực hiện xem operation này đã bị cancel chưa, nếu chưa thì sẽ bắt đầu thực hiện.

    Note: Không bao giờ gọi super.start() trong hàm start().
    Refer: https://developer.apple.com/documentation/foundation/operation/1416837-start

    Custom 1 Operation

    Mình sẽ custom 1 opeartion dùng để download ảnh, và check state isCancelled trong thân hàm để có thể dừng 1 operation đang chạy.

    1. Override lại hàm main cho operation. Đây là hàm operation sẽ chạy vào khi thực được gọi để thực hiện.
    2. set state thành .executing khi bắt đầu thực hiện operation.
    3. Luôn nhớ set state thành .finished khi operation được thực hiện xong
    1. check nếu operation đã bị cancel thì sẽ không download image.
    2. check nếu opeartion đã bị cancel thì sẽ không convert data thành image.
    3. Ở đây bạn có thể check nếu operation đã bị cancel thì không hiển thị image cũng được, tuy nhiên mình nghĩ download xong image rồi thì hiển thị cũng ok.

    Kết luận:

    • Operation thích hợp để dùng hơn GCD khi những task của bạn đòi hỏi sự kiểm soát state, hoặc những task có tính reuseable cao. Còn với những task đơn giản, hoặc không cần reuse nhiều thì có thể dùng GCD.
    • Operation là API bậc cao hơn GCD, nên cách dùng sẽ khó hiểu hơn, nên hãy cẩn trọng khi kiểm soát state của các custom Operation.

    Tham khảo: Ray wenderlich
    End

  • iOS/Swift: Animate your Launch screen

    iOS/Swift: Animate your Launch screen

    Lời mở đầu

    Chào mọi người, thông thường mành hình Launch screen là nơi để hiển thị logo của ứng dụng khi mà nó được khởi động, và mọi người thường để nó là một ảnh tĩnh để đảm bảo ứng dụng khởi động nhanh nhất có thể. Nhưng gần đây, mình có nhận được một yêu cầu của khách hàng về việc làm cho màn hình này trở nên sinh động hơn. Mình thấy nó cũng khá hay nên mình muốn chia sẻ với mọi người.

    Chia sẻ cách tạo Launch screen một cách sinh động

    Cách làm của chúng ta sẽ là tạo một LaunchScreen mới thay thế cho cái cũ. Trên màn hình LaunchScreen mới chúng ta sẽ thêm vào một số đối tượng và chúng ta sẽ làm cho nó chuyển động để tạo hiệu ứng. Ở phần hướng dẫn này mình sẽ dùng 2 UIImageView và 1 UILabel.
    Cụ thể mục đích của chúng ta sẽ làm chuyển động như hình này:

    Mình sẽ phải tách logo GST ra làm 2 phần và cho nó chuyển động từ 2 bên vào chính giữa và ghép thành một logo hoàn chỉnh.

    Đây là assets mình dùng trong project này:

    Bắt đầu

    Bước 1: Mở XCode và tạo mới Project (Single View App)

    Bước 2: Chúng ta cần file LaunchScreen.storyboard và tạo mới file LaunchScreen bằng cách:
    New File -> View -> đặt tên là LaunchScreen.xib.

    Bước 3: Mở tab General của Target Chọn lại Launch Screen File là LaunchScreen như hình dưới:

    Bước 4: Chỉnh sửa lại file LaunchSceen.xib để chuẩn bị cho việc sử dụng animtion

    Mở file LaunchScreen.xib chúng ta kéo vào 2 UIImageView và set image và tag cho từng lần lượt cho các view theo thứ tự từ trên xuống là 1, 2, 3 sao cho nó như hình dưới:

    Tiếp đến để đảm bảo nó chạy đúng với các kích thước màn hình khác nhau chúng ta cần set constraint cho các item trên màn hình.

    Ta cho imgeview với tag = 1 ẩn khỏi màn hình để sau chúng ta sẽ animate nó ra giữa màn hình vậy ta sẽ set constraint như hình dưới đây.

    Tương tự ta cũng set constraint imageview còn lại như hình dưới:

    Vậy là chúng ta đã chuẩn bị xong UI, giờ chúng ta cần đi vào code để thưc hiện animation.

    Bước 5. Tạo file LaunchScreenManager.swift để thực hiện animation bằng cách:
    Chọn new file -> Swift file -> đặt tên là LaunchScreenManager.swift

    Thêm code vào như dưới đây:

    import Foundation
    import UIKit
    
    class LaunchScreenManager {
        // dòng này dùng để tạo singleton: Nó đảm bảo sẽ chỉ tạo ra 1 instance duy nhất.
        static let instance = LaunchScreenManager()
    
        var view: UIView?// Đây sẽ là LaunchScreen của chúng ta
        var parentView: UIView?
    
        init() {}
    
        func loadView() -> UIView {
            let launchScreen = UINib(nibName: "LaunchScreen", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UIView
            return launchScreen
        }
    
        // Phương thức này thực hiện việc thêm view(launch screen) vào màn hình
        // Và set lại frame và vị trí cho LaunchScreen view
        func fillParentViewWithView() {
            guard let view = view, let parentView = parentView else { return }
            parentView.addSubview(view)
            view.frame = parentView.bounds
            view.center = parentView.center
        }
        
        // MARK: - Animation
    
        // Phương thức này để setup launch screen view và gọi hàm thực hiện animation
        func animateAfterLaunch(_ parentViewPassedIn: UIView) {
            parentView = parentViewPassedIn
            view = loadView()
            fillParentViewWithView()
            // start animation
            startAnimation()
        }
    
        func startAnimation() {
    
            // Lúc này chúng ta cần lấy các view đã add vào file LaunchScreen.xib để thực hiện animation
            // Để tránh sai sót trong việc đánh tag cho các view -> crash app
            //Chúng ta cần kiểm tra các item này có bị nil hay không
            guard let logo1 = view?.viewWithTag(1),
                let logo2 = view?.viewWithTag(2),
                let label = view?.viewWithTag(3) else { return }
    
            let screen = UIScreen.main.bounds.size
    
            UIView.animateKeyframes(withDuration: 2, delay: 0, options: [], animations: {
                //0
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1) {
                    logo1.transform = CGAffineTransform.identity.translatedBy(x: screen.width / 2 + logo1.frame.size.width / 2, y: 0)
                    logo2.transform = CGAffineTransform.identity.translatedBy(x: -(screen.width / 2 + logo2.frame.size.width / 2), y: 0)
                }
                UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.1) {
                    label.center.x = screen.width / 2
                }
                UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25) {
                    self.view?.alpha = 0
                }
            }) { (_) in
                self.view?.removeFromSuperview()
            }
        }
    }

    Bước cuối cùng:
    Mở file SceneDelegate.swift thêm đoạn code thực thi animation để khi app được khởi động sẽ chạy animation 😀

    Done, giờ chúng ta sẽ build app lên và tận hưởng 😀

    Kết quả

    Đối với việc tạo animation nếu mọi người quan tâm có thể tham khảo các bài viết của
    Vũ Đức Cương nhé!
    Part1
    Part2
    Part3

    Tổng kết

    Mình hi vọng bài viết của mình có thể giúp ích cho mọi người, để góp phần tạo ra các ứng dụng ngon hơn 😀

    Dưới đây là code project demo:

  • Một số animation cho UITableView

    Một số animation cho UITableView

    TableView là được sử dụng rất nhiều trong các ứng dụng của chúng ta. Vì vậy, việc tạo thêm một số hiệu ứng cho TableView khiến cho ứng dụng trở lên sinh động và bớt nhàm chán hơn. Chỉ với một vài câu lệnh, mọi thứ sẽ trở lên mới mẻ và dễ gần hơn rất nhiều lần.

    Hầu như các animation của tableview sẽ được tạo trong method dưới đây:

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        // Add animations here
    }

    Chúng ta bắt đầu với hiệu ứng đơn giản nhất nhưng được sử dụng nhiều nhất:

    Kết quả đạt được:

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
            cell.alpha = 0
            UIView.animate(withDuration: 0.5,
                           delay: 0.1 * Double(indexPath.row),
                animations: {
                    cell.alpha = 1
            })
        }

    Hiệu ứng Bounce animation:

    Bounce animation
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
          cell.transform = CGAffineTransform(translationX: 0, y: 60)
            UIView.animate(
                withDuration: 1,
                delay: 0.05 * Double(indexPath.row),
                usingSpringWithDamping: 0.4,
                initialSpringVelocity: 0.1,
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
            })
    }

    Move and Fade Animation

    Kết hợp hai hiệu ứng trên và chung ta ngừng sử dụng hiệu ứng Spring cho Cell, một hiệu ứng mới được tạo thành.

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cell.transform = CGAffineTransform(translationX: 0, y: 30)
            cell.alpha = 0
    
            UIView.animate(
                withDuration: 1,
                delay: 0.05 * Double(indexPath.row),
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
                    cell.alpha = 1
            })
    }

    Slide in Animation

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
            cell.transform = CGAffineTransform(translationX: tableView.bounds.width, y: 0)
    
            UIView.animate(
                withDuration: 1,
                delay: 0.5 * Double(indexPath.row),
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
            })
        }

    Trên đây là một số ví dụ về việc thêm animation vào tableview bằng các hiệu ứng cơ bản của UIView( transform, alpha …) ngoài ra với 1 số hiệu ứng khác như Flip, Change color … cũng được sử dụng rất nhiều trong các hiệu ứng animation. Chúng ta hoàn toàn có thể tạo ra các animation mang mang màu sắc của cá nhân như các họa sĩ code vậy.
    Một lưu ý đó là khi muốn kiểm soát tốt hơn các animation thì chúng ta nên tạo ra một class riêng  để xử lý việc chạy animation. Nó đồng thời cũng kiểm soát việc animation chỉ chạy một lần duy nhất với tất cả các cell.

  • Android Animation (Part 1)

    Android Animation (Part 1)

    Bài viết này mình muốn chia sẽ về Animation trong Android.

    Tổng quan

    Như các bạn đã biết thì Animation trong Android sẽ có loại View Animation (Android 2.3 và các bản trước đó), loại Property Animation (Android 3.0 và các bản sau này).

    Property Animation

    Là Animation được tạo bằng cách thay đổi các giá trị của thuộc tính của các đối tượng trong một khoảng thời gian đã được định sẵn bằng Animator.

    View Animation

    View Animation có Tween Animation, Frame Animation.

    • Tween Animation: Khởi tạo một animation bằng cách thực hiện một loạt các thay đổi trên một hình ảnh duy nhất với Animation.
    • Frame Animation: Khởi tạo một animation bằng cách sử dụng một chuỗi các hình ảnh được hiển thị theo một thứ tự nhất định với AnimationDrawable.
      Bài viết này mình sẽ giới thiệu cho các bạn về Tween Animation.

    Tween Animation

    Để thực hiện thì chúng tôi sẽ gọi method loadAnimation() của AnimationUtils.

    AnimationUtils.loadAnimation(applicationContext, R.anim.my_animation)

    R.anim.my_animation : các bạn sẽ tạo 1 thư mục anim trong thư mục res và tạo 1 file xml với tên là my_animation.
    Để áp dụng animation có đối tượng cần chạy animation thì các bạn gọi method startAnimation().

    animation_fade_in.startAnimation(AnimationUtils.loadAnimation(applicationContext, R.anim.fade_in))

    Ở đây tôi có 1 TextView đã được đặt tên là animation_fade_in và 1 file xml fade_in.xml để thực hiện animation fade in cho TextView.
    Chúng ta có một số animation thông dụng dưới đây:

    • Fade In
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true">
        <alpha
            android:duration="1000"
            android:fromAlpha="0.0"
            android:interpolator="@android:anim/accelerate_interpolator"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toAlpha="1.0" />
    </set>
    • Fade Out
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true">
        <alpha
            android:duration="1000"
            android:fromAlpha="1.0"
            android:interpolator="@android:anim/accelerate_interpolator"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toAlpha="0.0" />
    </set>
    • Blink
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
        <alpha
            android:duration="1000"
            android:fromAlpha="0.0"
            android:interpolator="@android:anim/accelerate_interpolator"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toAlpha="1.0" />
    </set>
    • Zoom In
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true">
        <scale
            android:duration="1000"
            android:fromXScale="1"
            android:fromYScale="1"
            android:pivotX="50%"
            android:pivotY="50%"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toXScale="2"
            android:toYScale="2" />
    </set>
    • Zoom Out
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true">
        <scale
            android:duration="1000"
            android:fromXScale="2.0"
            android:fromYScale="2.0"
            android:pivotX="50%"
            android:pivotY="50%"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toXScale="1"
            android:toYScale="1" />
    </set>
    • Rotate
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
        <rotate
            android:duration="1000"
            android:fromDegrees="0"
            android:interpolator="@android:anim/cycle_interpolator"
            android:pivotX="50%"
            android:pivotY="50%"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toDegrees="360" />
    </set>
    • Move
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true"
        android:interpolator="@android:anim/linear_interpolator">
        <translate
            android:duration="1000"
            android:fromXDelta="0%p"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toXDelta="50%p" />
    </set>
    • Slide Up
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true">
        <scale
            android:duration="1000"
            android:fromXScale="1.0"
            android:fromYScale="1.0"
            android:interpolator="@android:anim/linear_interpolator"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toXScale="1.0"
            android:toYScale="0.0" />
    </set>
    • Slide Down
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true">
        <scale
            android:duration="1000"
            android:fromXScale="1.0"
            android:fromYScale="0.0"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toXScale="1.0"
            android:toYScale="1.0" />
    </set>
    • Sequential
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true"
        android:interpolator="@android:anim/linear_interpolator"
        android:repeatCount="infinite"
        android:repeatMode="restart">
        <!-- Move -->
        <translate
            android:duration="1000"
            android:fillAfter="true"
            android:fromXDelta="0%p"
            android:startOffset="300"
            android:toXDelta="75%p" />
        <translate
            android:duration="1000"
            android:fillAfter="true"
            android:fromYDelta="0%p"
            android:startOffset="1100"
            android:toYDelta="70%p" />
        <translate
            android:duration="1000"
            android:fillAfter="true"
            android:fromXDelta="0%p"
            android:startOffset="1900"
            android:toXDelta="-75%p" />
        <translate
            android:duration="1000"
            android:fillAfter="true"
            android:fromYDelta="0%p"
            android:startOffset="2700"
            android:toYDelta="-70%p" />
    
        <!-- Rotate 360 degrees -->
        <rotate
            android:duration="1200"
            android:fromDegrees="0"
            android:interpolator="@android:anim/cycle_interpolator"
            android:pivotX="50%"
            android:pivotY="50%"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:startOffset="3800"
            android:toDegrees="360" />
    
    </set>
    • Together
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:fillAfter="true"
        android:interpolator="@android:anim/linear_interpolator">
    
        <!-- Move -->
        <scale
            android:duration="4000"
            android:fromXScale="1"
            android:fromYScale="1"
            android:pivotX="50%"
            android:pivotY="50%"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toXScale="3"
            android:toYScale="3" />
    
        <!-- Rotate 180 degrees -->
        <rotate
            android:duration="1000"
            android:fromDegrees="0"
            android:pivotX="50%"
            android:pivotY="50%"
            android:repeatCount="infinite"
            android:repeatMode="restart"
            android:toDegrees="360" />
    
    </set>

    Các thuộc tính bạn cần phải biết:
    android:duration – Thời gian hoàn thành.
    android:startOffset – Thời gian chờ trước khi một animaiton bắt đầu và thường được sử dụng khi có nhiều animation.
    android:repeatMode – Thiết lập lặp lại animation.
    android:repeatCount – Xác định số lần lặp lại animation. Nếu bạn thiết lập giá trị này là infinite thì animation sẽ lặp lại lần vô hạn.
    android:interpolator – Tỷ lệ thay đổi animation.
    android:fillAfter – Xác định liệu có áp dụng việc chuyển đổi đối tượng về trạng thái ban đầu sau khi một animation đã hoàn thành hay không.

    Các thành phần chính trong interpolator để các bạn dùng cho phù hợp:

    • accelerateDecelerateInterpolator: Tốc độ thay đổi bắt đầu và kết thúc chậm nhưng tăng tốc qua giữa.
    • accelerateInterpolator: Tốc độ thay đổi bắt đầu chậm, sau đó tăng tốc.
    • anticipateInterpolator: Bắt đầu một khoảng lùi lại sau đó bay về phía trước.
    • anticipateOvershootInterpolator: Bắt đầu lùi lại, bay về phía trước và vượt qua giá trị đích, sau đó lùi lại giá trị đích.
    • bounceInterpolator: Sau khi đến vị trí giá trị cuối thì quay lại giá trị ban đầu
    • cycleInterpolator: Lặp lại hoạt ảnh động theo một số chu kỳ xác định. Tốc độ thay đổi theo mô hình hình sin.
    • decelerateInterpolator: Hoạt ảnh có tốc độ thay đổi bắt đầu nhanh chóng, sau đó giảm tốc.
    • linearInterpolator: Tốc độ thay đổi hoạt ảnh là không đổi(tuyến tính).

    Các cú pháp cho các bạn tham khảo:

    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:interpolator="@[package:]anim/interpolator_resource"
        android:shareInterpolator=["true" | "false"] >
        <alpha
            android:fromAlpha="float"
            android:toAlpha="float" />
        <scale
            android:fromXScale="float"
            android:toXScale="float"
            android:fromYScale="float"
            android:toYScale="float"
            android:pivotX="float"
            android:pivotY="float" />
        <translate
            android:fromXDelta="float"
            android:toXDelta="float"
            android:fromYDelta="float"
            android:toYDelta="float" />
        <rotate
            android:fromDegrees="float"
            android:toDegrees="float"
            android:pivotX="float"
            android:pivotY="float" />
        <set>
            ...
        </set>
    </set>
    • Fade In
    • Fade Out
    • Blink
    • Zoom In
    • Zoom Out
    • Rotate
    • Move
    • Slide Up
    • Slide Down
    • Sequential
    • Together

    Link tham khảo:

    • https://developer.android.com/training/animation
  • Operation (P2)

    Operation (P2)

    Ở phần 1 của bài viết, mình đã giới thiệu về Operation là gì. Ở phần 2 của loạt bài về Operation, mình sẽ nói về Dependency trong Operation.

    Nội dung bài viết:

    • Operation Dependencies
    • Passing Data using Dependencies

    Operation Dependencies:

    Operation cho phép bạn thiết lập các sự phụ thuộc lẫn nhau. Điều này mang lại 2 lợi ích:

    • Giả sử operation 2 phụ thuộc vào operation 1. Khi đó operation 2 chỉ được thực hiện sau khi operation 1 đã hoàn thành.
    • Cung cấp 1 cách để bạn truyền data giữa các operation.
    class DownloadImage: Operation {
        var index: Int
        
        init(ind: Int) {
            self.index = ind
        }
    
        override func main() {
            // Download image task
            print("Start downloading task \(index) at time: \(Date().timeIntervalSince1970)")
            sleep(5)
            print("Finish downloading task \(index) at time: \(Date().timeIntervalSince1970)")
        }
    }
    
    let firstOperation = DownloadImage(ind: 1)
    let secondOperation = DownloadImage(ind: 2)
    //firstOperation.addDependency(secondOperation)
    
    let operationQueue = OperationQueue()
    operationQueue.addOperation(firstOperation)
    operationQueue.addOperation(secondOperation)

    Ở trên là đoạn code khởi tạo 2 operation bình thường, giữa chúng chưa có dependency. Chạy đoạn code trên và đây là kết quả thu được:

    2 task chạy song song

    Giờ thì bỏ comment dòng code firstOperation.addDependency(secondOperation) và chạy thử:

    Task 1 phụ thuộc vào task 2. Vì vậy, task 1 chỉ chạy khi task 2 đã hoàn thành.

    Note: Bạn có thể tạo dependency cho 2 opeartion đang chạy ở 2 operation queue khác nhau.

    Đây là 1 cách khá ngắn trong khi ở GCD bạn phải khai báo 1 dispatchGroup, sau đó gọi hàm enter(), leave(), notify(), … Tuy nhiên, cách làm này rất dễ gây ra deadlock.

    Để remove 1 dependency, bạn chỉ cần gọi:

    firstOperation.removeDependency(op: secondOperation)
    Task 5 chỉ được chạy khi task 2 hoàn thành, task 2 chỉ chạy khi task 3 hoàn thành, task 3 chỉ chạy khi task 5 hoàn thành.

    Ở đoạn code mẫu ở trên, nếu bạn sửa đoạn code thành như dưới đây thì code của bạn sẽ bị deadlock và không chạy.

    firstOperation.addDependency(secondOperation)
    secondOperation.addDependency(firstOperation)

    Note: Hãy vẽ sơ đồ ra 1 tờ giấy để luôn clear về flow của bạn, tránh bị deadlock.

    Truyền data giữa các operation thông qua dependency:

    class Calculate: Operation {
        let firstNum: Int
        let secondNum: Int
        var sum: Int?
        
        init(first: Int, second: Int) {
            self.firstNum = first
            self.secondNum = second
        }
        
        override func main() {
            sum = firstNum + secondNum
        }
    }
    
    class Display: Operation {
        
        override func main() {
            // 2
            let sum = dependencies.compactMap{ ($0 as? Calculate)?.sum }.first
            
            guard let unwrappedSum = sum else {
                return
            }
            // 3
            print("Sum = \(unwrappedSum)")
        }
    }
    
    let calculateOperation = Calculate(first: 5, second: 10)
    let displayOperation = Display()
    // 1
    displayOperation.addDependency(calculateOperation)
    
    
    let operationQueue = OperationQueue()
    // 4
    operationQueue.addOperation(calculateOperation)
    operationQueue.addOperation(displayOperation)
    1. Khởi tạo 2 operation, và set dependency để task Display chỉ chạy sau khi task Calculate hoàn thành.
    2. Hàm main() là hãm sẽ chạy khi 1 operation được chạy.
      Ở đây, ta sẽ lấy ra list operations có dependency với DisplayOperation, chọn ra operation nào là Calculate và lấy ra sum.
    3. Nếu sum khác nil thì hiển thị ra.
    4. Add các operation vào queue để chạy.

    Kết quả hiển thị trên màn hình Console:

    Dependency trong Operation là 1 trong những thứ giúp Operation vượt trội hơn so với GCD.

    Ở phần tiếp, mình sẽ nói về Async Operation và xử lí cancel Opeartion.

  • Custom Lint Rules

    Custom Lint Rules

    Bài viết này tôi sẽ giới thiệu về phương pháp mở rộng lint và tạo custom rule.

    Lint và cách custom rule của lint như thế nào?

    Lint là một bộ phân tích tĩnh, có mục tiêu tìm lỗi trên mã nguồn của bạn mà không cần phải biên dịch hoặc chạy nó.
    Để tạo custom rule của lint thì cần phải tạo Detector, Issue, Registry.

    Creating your rules module

    Chúng ta bắt đầu bằng cách định nghĩa một module Java / Kotlin riêng biệt. Sau đó chúng ta sẽ thêm vào build.gradle như sau:

    apply plugin: 'java-library'
    apply plugin: 'kotlin'
    
    dependencies {
        compileOnly "com.android.tools.lint:lint-api:26.5.3"
        compileOnly "com.android.tools.lint:lint-checks:26.5.3"
    }
    
    jar {
        manifest {
            attributes("Lint-Registry-v2": "techover.rules.IssueRegistry")
        }
    }

    Creating Detector

    Detector là một lớp có thể tìm thấy một hay nhiều vấn đề cụ thể hơn. Tùy thuộc vào vấn đề, chúng ta có thể sử dụng các loại phát hiện khác nhau:
    SourceCodeScanner – một trình phát hiện chuyên về các tệp nguồn Java / Kotlin.
    XmlScanner – một trình phát hiện chuyên về các tệp XML.
    GradleScanner – một trình phát hiện chuyên về các tệp Gradle.
    ResourceFolderScanner – một trình phát hiện chuyên về các thư mục tài nguyên (không phải các tệp mà nó chứa).
    Bạn có thể tham khảo tạo Detector dưới đây:

    package techover.rules
    
    import com.android.tools.lint.detector.api.*
    import com.intellij.psi.PsiMethod
    import org.jetbrains.uast.UCallExpression
    
    /**
     * This detector will report any usages of the android.util.Log.
     */
    class AndroidLogDetector : Detector(), SourceCodeScanner {
    
        override fun getApplicableMethodNames(): List<String> =
            listOf("tag", "format", "v", "d", "i", "w", "e", "wtf")
    
        override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
            super.visitMethodCall(context, node, method)
            val evaluator = context.evaluator
            if (evaluator.isMemberInClass(method, "android.util.Log")) {
                reportUsage(context, node)
            }
        }
    
        private fun reportUsage(context: JavaContext, node: UCallExpression) {
            context.report(
                issue = ISSUE,
                scope = node,
                location = context.getCallLocation(
                    call = node,
                    includeReceiver = true,
                    includeArguments = true
                ),
                message = "android.util.Log usage is forbidden."
            )
        }
    
        companion object {
            private val IMPLEMENTATION = Implementation(
                AndroidLogDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
    
            val ISSUE: Issue = Issue
                .create(
                    id = "AndroidLogDetector",
                    briefDescription = "The android Log should not be used",
                    explanation = """
                    For amazing showcasing purposes we should not use the Android Log. We should the
                    AmazingLog instead.
                """.trimIndent(),
                    category = Category.CORRECTNESS,
                    priority = 9,
                    severity = Severity.ERROR,
                    androidSpecific = true,
                    implementation = IMPLEMENTATION
                )
        }
    }
    • Nó extends Detector để Android Lint có thể sử dụng để phát hiện sự cố.
    • Nó extends SourceCodeScanner vì chúng ta cần kiểm tra cả hai tệp Kotlin và Java.
    • getApplossibleMethodNames – chỉ lọc các chữ ký phương thức tồn tại trong android.util.Log
    • visitMethodCall – sử dụng trình đánh giá để đảm bảo rằng phương thức này được gọi bởi android.util.Log chứ không phải bởi bất kỳ lớp nào khác. Ví dụ: AmazingLog có cùng phương thức và không nên gắn cờ.
    • reportUsage – được sử dụng để báo cáo sự cố khi tìm thấy.

    Creating Issue

    Issue là một lỗi tiềm ẩn trong ứng dụng Android. Đây là cách bạn khai báo lỗi mà quy tắc của bạn sẽ giải quyết.
    id – để xác định duy nhất vấn đề này.
    briefDescription– mô tả tóm tắt về vấn đề.
    explanation – nên là một mô tả sâu hơn về vấn đề và lý tưởng về cách giải quyết vấn đề.
    category – xác định loại vấn đề. Có rất nhiều danh mục có thể có như CORRECTNESS, USABILITY, I18N, COMPLIANCE, PERFORMANCE, …
    priority – một số từ 1 đến 10, trong đó số càng lớn thì vấn đề càng nghiêm trọng.
    severity – nó có thể là một trong những giá trị sau: FATAL, ERROR, WARNING, INFORMATIONAL and IGNORE. Lưu ý: Nếu mức độ nghiêm trọng là FATAL hoặc ERROR thì việc chạy lint sẽ thất bại và bạn sẽ phải giải quyết vấn đề.
    implementation – lớp chịu trách nhiệm phân tích tệp và phát hiện vấn đề.
    Bạn có thể tham khảo tạo Issue dưới đây:

    val ISSUE: Issue = Issue
                .create(
                    id = "AndroidLogDetector",
                    briefDescription = "The android Log should not be used",
                    explanation = """
                    For amazing showcasing purposes we should not use the Android Log. We should the
                    AmazingLog instead.
                """.trimIndent(),
                    category = Category.CORRECTNESS,
                    priority = 9,
                    severity = Severity.ERROR,
                    androidSpecific = true,
                    implementation = IMPLEMENTATION
                )

    Creating Registry

    Bạn có thể tham khảo Registry dưới đây:

    package techover.rules
    
    import com.android.tools.lint.client.api.IssueRegistry
    import com.android.tools.lint.detector.api.CURRENT_API
    import com.android.tools.lint.detector.api.Issue
    
    class IssueRegistry : IssueRegistry() {
    
        override val api: Int = CURRENT_API
    
        override val issues: List<Issue>
            get() = listOf(AndroidLogDetector.ISSUE)
    }

    Run Lint

    Chúng ta sẽ thêm vào app/build.gradle như sau:

    dependencies {
        lintChecks project(path: ':rules')
    }

    Sau đó chúng ta sẽ chạy command dưới đây:

    ./gradlew app:lintDebug

    Nếu source không có Issue mà chúng ta đã Registry thì sẽ in ra nội dung như sau:

    > Task :app:lintDebug
    Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
    Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
    
    BUILD SUCCESSFUL 

    Ví dụ, trong class MainActivity bạn có log như sau:

    Log.d("MainActivity", "https://magz.techover.io/")

    Thì chúng ta thấy source có Issue mà chúng ta đã Registry nê sẽ in ra nội dung như sau:

    > Task :app:lintDebug FAILED
    Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
    Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
    
    FAILURE: Build failed with an exception.
    
    * What went wrong:
    Execution failed for task ':app:lintDebug'.
    > Lint found errors in the project; aborting build.
    
      Fix the issues identified by lint, or add the following to your build script to proceed with errors:
      ...
      android {
          lintOptions {
              abortOnError false
          }
      }
      ...
    
      Errors found:
    
      ~/Techover/Lint/app/src/main/java/techover/lint/MainActivity.kt:13: Error: android.util.Log usage is forbidden. [AndroidLogDetector]
              Log.d("MainActivity", "https://magz.techover.io/")
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    
    
    * Try:
    Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
    
    * Get more help at https://help.gradle.org
    
    BUILD FAILED 

    Link tham khảo:

    • https://github.com/fabiocarballo/lint-sample
  • iOS/Swift: UIPanGestureReconizer

    iOS/Swift: UIPanGestureReconizer

    Lời mở đầu

    Xin chào mọi người, hôm nay mình sẽ giới thiệu với các bạn về UIPanGestureReconizer. Và mình sẽ hướng dẫn mọi người cách sử dụng và ứng dụng nó vào thực tế. Mình hi vọng sau khi xem hết bài viết này, mọi người có thể áp dụng nó vào các ứng dụng sau này nếu cần đến 😀

    Ý tưởng

    Chắc hẳn ai sử dụng iOS đều sử dụng qua tính năng “Control Center” của iOS. Để mở Control center lên ta phải vuốt từ dưới màn hình lên và ẩn đi thì ngược lại, nó còn cho phép người dùng kéo thả một cách mượt mà.

    UIGestureRecognizer là gì?

    UIGestureRecognizer là class định nghĩa một tập hợp các hành vi phổ biến có thể được cấu hình cho tất cả các cử chỉ cụ thể. Dạng như chạm, kéo thả …

    UIGestureRecognizer được sử dụng để nhận dạng các loại hành vi của người dùng khi tương tác lên màn hình và thực hiện hành động được cấu hình.

    Dưới đây là một số subclasses của nó:

    UIPanGestureRecognizer là gì?

    UIPangestureRecognizer là subclass của UIGestureRecognizer dùng để nhận biết hành vi kéo thả.

    Ví dụ về UIPanGestureRecognizer

    Để bắt đầu chúng ta cần mở XCode và tạo mới một Single View App. Sau khi tạo project xong chúng ta mở file Main.storyboard và kéo vào một UIView -> đổi màu nền(Background color) của view sang một màu khác cho dễ nhìn.

    Khi này chúng ta cần gán constraint cho view đó theo Hình 1
    Cần chú ý ở đây chúng ta sẽ constraint bottom của view vào bottom của superview thay vì Safe Area để khi build trên các dòng điện thoại tai thỏ (iphone x, xs ….) sẽ không bị trắng ở dưới của màn hình.

    Hình 1

    Tiếp đến chúng ta sẽ add 1 UIView nhỏ vào trong UIView vừa mới tạo để thể hiện view này có thể kéo thả được.
    Chúng ta cũng constraint UIView nhỏ để nó nằm trên top và giữa của superview kết quả chúng ta thu được như Hình 2

    Hình 2

    Vậy là chúng ta đã xong phần UI, tiếp đến chúng ta cần kéo IBOutlet top constraint cho cái view to và đặt tên nó là topViewContainer kết quả sẽ được như hình 3

    Chúng ta kéo IBOutlet cho top constraint của view để làm gì? Mục đích ở đây là để sau này chúng ta sẽ thay đổi giá trị của topConstraint.constant -> thay đổi vị trí và để tạo hiệu ứng chuyển động.

    Tiếp đến chúng ta kéo outlet cho thằng View to đặt tên là viewContainer

    OK, giờ chúng ta sẽ đi vào code chi tiết.
    Giờ chúng ta mở file ViewController.swift ra và tạo enum cho các trạng thái mở rộng

    // 1: Tạo 3 trạng thái mở rộng cho viewContainer
    enum ExpansionState {
        case compressed
        case haft
        case expanded
    }

    Tạo pangesture và gán nó cho viewContainer, trong code mình đã comment rõ từng dòng, từng hàm để các bạn có thể hiểu được nó làm gì.

       // thêm UIGestureRecognizerDelegate để sử dụng được PanGesture
    class ViewController: UIViewController, UIGestureRecognizerDelegate {
    
        @IBOutlet weak var viewContainer: UIView!
        @IBOutlet weak var topViewContainer: NSLayoutConstraint!
        // Khởi tạo trạng thái
        var expansionState: ExpansionState = .haft
        // Khởi tạo giá trị cũ của topViewContainer
        var oldTopViewContainer: CGFloat = 0
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            setupGestureRecognizers()
            // Khởi tạo vị trí bắt đầu cho việc animte viewContainer.
            // Ở đây chúng ta đang để nó ở trạng thái compressed có nghĩa là ở trạng thái expansion nhỏ nhất.
            topViewContainer.constant = topViewContainer(forState: .compressed)
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            // Do mình muốn viewContainer sẽ chuyển động từ dưới lên trên và dừng lại ở trạng thái expansion 1 nửa màn hình
            // Nên mình gọi hàm phía dưới để nó thực hiện chuyển động vì ở viewDidLoad nó đang ở trạng thái compressed
            animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: CGPoint(x: 0, y: 50))
        }
    
        // Cài đặt Pangesture,
        func setupGestureRecognizers() {
            let panGestureRecognizer = UIPanGestureRecognizer(target: self,
                                                              action: #selector(panGestureDidMove(sender:)))
            panGestureRecognizer.delegate = self
            viewContainer.isUserInteractionEnabled = true
    
            viewContainer.addGestureRecognizer(panGestureRecognizer)
        }
    
        @objc func panGestureDidMove(sender: UIPanGestureRecognizer) {
            let translationPoint = sender.translation(in: view.superview)
            // velocity là vận tốc chuyển động của hành động, dạng như bạn vuốt vào màn hình nhanh hay chậm
            let velocity = sender.velocity(in: view.superview)
    
            switch sender.state {
            case .changed:
                panGesture(didChangeTranslationPoint: translationPoint, withVelocity: velocity)
            case .ended:
                panGesture(didEndTranslationPoint: translationPoint, withVelocity: velocity)
            default:
                return
            }
        }
    
        // Phương thức này để tính toán giá trị viewContainer.top theo các trạng thái thái mà nó cần tính
        func topViewContainer(forState state: ExpansionState) -> CGFloat {
            let heightView = self.view.bounds.height
            switch state {
            case .compressed:
                return heightView - 100
            case .haft:
                return heightView / 2
            case .expanded:
                return 100
            }
        }
    }

    Tiếp đến chúng ta cần tạo Extenstion cho phần animation để phục vụ cho việc chuyển động view container

    / MARK: Animation
    extension ViewController {
        /// Animates the top constraint of the drawerViewController by a given constant
        /// using velocity to calculate a spring and damping animation effect.
        func animateTopConstraint(constant: CGFloat, velocity: CGPoint) {
            let previousConstraint = topViewContainer.constant
            let distance = previousConstraint - constant
            let springVelocity = velocity.y != 0 ? max(1 / (abs(velocity.y / distance)), 0.08) : 0.08
            let springDampening = CGFloat(0.6)
    
            UIView.animate(withDuration: 0.5,
                           delay: 0.0,
                           usingSpringWithDamping: springDampening,
                           initialSpringVelocity: springVelocity,
                           options: [.curveLinear],
                           animations: {
                            self.topViewContainer.constant = constant
                            self.oldTopViewContainer = constant
                            self.view.layoutIfNeeded()
            }, completion: nil)
        }
    }

    Tiếp đến chúng ta cần các hàm để xử lí khi mà người dùng kéo viewContainer
    Ở trong code mình đã comment các hàm, các dòng để mọi người dễ hiểu, hãy đọc trong code nhé.

    //MARK: PanGesture handle
    extension ViewController {
    
        // Hàm này được gọi khi PanGestureRecognizer nhận ra được sự thay đổi của view,
        // trong hàm này chúng ta có thể thực hiện một số điều kiện để giới hạn việc kéo của người dùng
        func panGesture(didChangeTranslationPoint translationPoint: CGPoint,
                                  withVelocity velocity: CGPoint) {
            let newConstraintConstant = oldTopViewContainer + translationPoint.y
            // Giới hạn việc người dùng kéo quá xa so với phía trên
            if newConstraintConstant >= 0 {
                topViewContainer.constant = newConstraintConstant
            }
        }
    
        // Phương thức này được gọi khi kết thúc pan
        func panGesture(didEndTranslationPoint translationPoint: CGPoint,
                                  withVelocity velocity: CGPoint) {
            // Velocity là kiểu CGPoint vì vậy nó có 2 giá trị x và y
            // x: đại diện cho vận tốc theo chiều ngang
            // y: đại điện cho vận tốc theo chiều dọc
            // Do mình đang muốn thực hiện kéo thả theo chiều dọc nên ở ví dụ này mình sẽ dùng thuộc tính vận tốc y
            if abs(velocity.y) <= 50.0 {
                // 50: ở đây là vận tốc mà mình coi nó là người dùng đang kéo từ từ. Con số này các bạn có thể tùy chỉnh
                // Giá trị của y < 0 có nghĩa là người dùng đang kéo lên trên và y > 0 là kéo xuống dưới
                // dấu của y sẽ thể hiện chiều di chuyển của nó.
                // Vì vậy mình cần sử dụng hàm abs() trước khi thực hiện phép so sánh
                drag(lowVelocity: velocity)
            } else if velocity.y > 0 {
                dragDown(highVelocity: velocity)
            } else {
                dragUp(highVelocity: velocity)
            }
        }
    
        // Hàm này thực hiện việc xác định xem view đang gần vị trí của trạng thái nào hơn sẽ chuyển sang trạng thái đó.
        func drag(lowVelocity velocity: CGPoint) {
            let compressedTopConstraint = topViewContainer(forState: .compressed)
            let haftTopConstraint = topViewContainer(forState: .haft)
            let expandedTopConstraint = topViewContainer(forState: .expanded)
    
            let expandedDifference = abs(topViewContainer.constant - expandedTopConstraint)
            let haftDifference = abs(topViewContainer.constant - haftTopConstraint)
            let compressedDifference = abs(topViewContainer.constant - compressedTopConstraint)
    
            let heightArray = [expandedDifference, haftDifference, compressedDifference]
            let minHeight = heightArray.min()
    
            if expandedDifference == minHeight {
                expansionState = .expanded
                animateTopConstraint(constant: expandedTopConstraint, velocity: velocity)
            } else if haftDifference == minHeight {
                expansionState = .haft
                animateTopConstraint(constant: haftTopConstraint, velocity: velocity)
            } else {
                expansionState = .compressed
                animateTopConstraint(constant: compressedTopConstraint, velocity: velocity)
            }
        }
    
        // Hàm này để xử lí khi người dùng kéo thả view với tốc độ cao,
        // nó sẽ xác định và chuyển tới trạng thái liền kề theo hướng phía dưới
        func dragDown(highVelocity velocity: CGPoint) {
            // Handle High Velocity Pan Gesture
    
            switch expansionState {
            case .compressed, .haft:
                expansionState = .compressed
                animateTopConstraint(constant: topViewContainer(forState: .compressed), velocity: velocity)
            case .expanded:
                expansionState = .haft
                animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: velocity)
            }
        }
    
        // Hàm này để xử lí khi người dùng kéo thả view với tốc độ cao,
        // nó sẽ xác định và chuyển tới trạng thái liền kề theo hướng phía trên
        func dragUp(highVelocity velocity: CGPoint) {
            // Handle high Velocity Pan Gesture
            switch expansionState {
            case .compressed:
                expansionState = .haft
                animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: velocity)
            case .haft, .expanded:
                expansionState = .expanded
                animateTopConstraint(constant: topViewContainer(forState: .expanded), velocity: velocity)
            }
        }
    }

    Và đây là kết quả khi chúng ta build app lên:

    Vậy là xong!
    Ngoài ra các bạn có thể chỉnh sửa lại view sao cho phù hợp với ứng dụng đang làm 😀


    Cảm ơn mọi người đã theo dõi bài viết!
    Mọi ý kiến đóng góp mọi người hãy comment xuống phía dưới để mình có thể thay đổi cho bài biết được tốt hơn. :v