Blog

  • iOS — Play RTSP Streaming

    iOS — Play RTSP Streaming

    Hướng dẫn build IJK Player để play RTSP streaming

    Table of Contents

    Chuẩn bị môi trường

    • Cài đặt Homebrew ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    • Cài git brew install git
    • Cài yasm brew install yasm
    • Clone IJKPlayer từ github:
      • git clone https://github.com/RioV/ijkplayer.git ijkplayer-ios
        • Note: ở đây dùng /RioV/ijkplayer bởi vì đang tìm hiểu thấy IJKPlayer có issue, vậy nên folk sang một bản khác để tiện fix bug
        • Note: chú ý checkout source code về folder mà tên không có space, ví dụ: IJK Player => NG, IJK-Player => OK. Việc này sẽ ảnh hưởng đến tiến trình build lib, nếu như có space thì build sẽ bị lỗi.
      • cd ijkplayer-ios
      • git checkout -B latest k0.8.8 version lấy theo release tag của IJKPlayer Release Nếu sau này sửa lỗi lib ở branch develop thì sẽ là git checkout -B develop

    Build lib IJKPlayer

    • cd config
    • rm module.sh
    • ln -s module-lite.sh module.sh -> việc này sẽ bỏ module.sh default và thay vào đó sử dụng moule-lite.sh nhằm giảm binary size
    • Để build lib support RTSP thì cần chỉnh sửa file module-lite.sh như sau:
      • Xoá: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-protocol=rtp"
      • Add: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=rtp"
      • Add: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtsp" (nên để trong section của demuxer)
    • cd ..
    • ./init-ios.sh
    • cd ios
    • ./compile-ffmpeg.sh clean
    • Sửa file ijkplayer-ios/ios/compile-ffmpeg.sh
      • Chuyển:

        FF_ALL_ARCHS_IOS6_SDK="armv7 armv7s i386" FF_ALL_ARCHS_IOS7_SDK="armv7 armv7s arm64 i386 x86_64" FF_ALL_ARCHS_IOS8_SDK="armv7 arm64 i386 x86_64"

        Thành

        FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"

    • ./compile-ffmpeg.sh all
      • Note: Với câu lệnh ./compile-ffmpeg.sh all thì rất dễ xảy ra lỗi nếu như source code đang ở trong directoy có chứa space. Ví dụ: working directory là /Documents/JLK Player thì sẽ lỗi, để fix thì chuyển thành /Documents/IJKPlayer

    Tích hợp IJKPlayer vào project

    • Add IJKPlayer vào project: File -> add File to "Project" -> chọn ijkplayer-ios/ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj
    • Chọn: Application’s target.
      • Vào: Build Phases -> Target Dependencies -> Chọn IJKMediaFramework
      • Chọn IJKMediaPlayer.xcodeproj, chọn target IJKMediaFramework và build.
      • Vào: Build Phases -> Link Binary with Libraries -> Thêm:
        • libc++.tbd
        • libz.tbd
        • libbz2.tbd
        • AudioToolbox.framework
        • UIKit.framework
        • CoreGraphics.framework
        • AVFoundation.framework
        • CoreMedia.framework
        • CoreVideo.framework
        • MediaPlayer.framework
        • MobileCoreServices.framework
        • OpenGLES.framework
        • QuartzCore.framework
        • VideoToolbox.framework

    Sample

    Sử dụng đoạn source code sau để play thử RTSP stream bằng IJKPlayer

    import UIKit
    import IJKMediaFramework
    
    class IJKPlayerViewController: UIViewController {
        var player: IJKFFMoviePlayerController!
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let options = IJKFFOptions.byDefault()
            let url = URL(string: "rtsp://170.93.143.139/rtplive/470011e600ef003a004ee33696235daa")
            guard let player = IJKFFMoviePlayerController(contentURL: url, with: options) else {
                print("Create RTSP Player failed")
                return
            }
    
            let autoresize = UIView.AutoresizingMask.flexibleWidth.rawValue |
                UIView.AutoresizingMask.flexibleHeight.rawValue
            player.view.autoresizingMask = UIView.AutoresizingMask(rawValue: autoresize)
    
            player.view.frame = self.view.bounds
            player.scalingMode = IJKMPMovieScalingMode.aspectFit
            player.shouldAutoplay = true
            self.view.autoresizesSubviews = true
            self.view.addSubview(player.view)
            self.player = player        
            self.player.prepareToPlay()
        }
    }
  • Khởi tạo (Initialization) trong Swift

    Khởi tạo (Initialization) trong Swift

    Việc khởi tạo trong bất cứ ngôn ngữ lập trình nào đều rất rất quan trọng, trong một project thì bạn sẽ liên tục phải thực hiện khởi tạo các instance của các struct, class hoặc enum. Việc hiểu rõ và sử dụng thành thạo quá trình khởi tạo trong swift sẽ giúp bạn tăng performance cũng như chất lượng của source code, dưới đây sẽ là bài viết đào sâu hơn vào quá trình khởi tạo trong swift nhé.

    I. Khởi tạo trong Structure

    1. Khởi tạo mặc định

    Việc khởi tạo này đơn gỉản và giống với việc tạo 1 instance của method không có parameter nào, ví dụ

    let phoneX = Phone()

    Syntax

    init() {
       // New Instance sẽ được khởi tạo ở đây
    }

    Example

    struct Rectangle {
        var length: Double
        var breadth: Double
        init() {
            length = 12.0
            breadth = 5.0
        }
    }
    
    var rec = Rectangle()
    print("area of rectangle is \(rec.length * rec.breadth)")

    Ở đoạn code bên trên 1 instance là rec được khởi tạo với các thuộc tính là chiều rộng và chiều dài lần lượt là length = 12, và breadth = 5, sau khi chạy đoạn code trên sẽ thu được kết quả:

    area of rectangle is 60.0

    Chuyện gì sẽ xảy ra nếu xoá phần init() trong đoạn code trên:

    struct Rectangle {
        var length: Double
        var breadth: Double
    }
    
    var rec = Rectangle()
    print("area of rectangle is \(rec.length * rec.breadth)")

    Khi chạy đoạn code trên sẽ có thông báo lỗi là

    error: missing argument for parameter 'length' in call
    let rec = Rectangle()

    Ở đây thì trình biên dịch đang thông báo 1 error và bắt chúng ta phải khởi tạo với đối số của length, taị sao lại như vậy???
    Bởi vì bạn đang cố khởi tạo cho struct Rectangle mà các stored property chưa được gán giá trị mặc định
    Đoạn code sửa lỗi này được sửa như sau:

    struct Rectangle {
        var length: Double = 12.0
        var breadth: Double = 5.0
    }
    
    var rec = Rectangle()
    print("area of rectangle is \(rec.length * rec.breadth)")

    Chúng ta gán giá trị mặc định cho tất cả các stored property, ở đây thì chúng ta đã tạo ra instance rec với các stored property được gán giá trị ban đầu lần lượt là 12.0 và 5.0,
    vậy là vấn đề được giải quyết.

    Như trên thì chúng ta đã được tiếp cận với việc khởi tạo 1 struct với kiểu mặc định, nhìn lại 2 cách viết ở trên nhé:

    // 1
    struct Rectangle {
        var length: Double
        var breadth: Double
        init() {
            length = 12.0
            breadth = 5.0
        }
    }
    
    // 2
    struct Rectangle {
        var length: Double = 12.0
        var breadth: Double = 5.0
    }

    2 cách viết nói trên đều đưa đến 1 kết quả như nhau là đều tạo ra 1 instance có các giá trị khởi tạo mặc định là 12.0 và 5.0,
    tuy nhiên thì cách viết thứ 2 sẽ giảm bớt line code và tường minh hơn, vì vậy trong trường hợp cần khởi tạo với giá trị mặc định thì chúng ta nên chọn cách viết thứ 2 nhé.

    2. Khởi tạo với các Parameter

    Khi cần khởi tạo 1 instance với các giá trị của property được truyền vào như dưới đây:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    }
    
    let rect = Rectangle(length: 6, breadth: 12)
    print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Kết quả sau khi chạy:

    length is: 6.0
    breadth is: 12.0

    Giờ bạn thử xoá phần init trong đọan code trên xem có gì xảy ra nhé.

    struct Rectangle {
        var length: Double
        var breadth: Double
    }
    
    let rect = Rectangle(length: 6, breadth: 12)
    print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Oh, Code trên vẫn chạy bình thường. Do với struct thì Swift mặc định đã tạo sẵn cho bạn 1 hàm khởi tạo rồi, tuy nhiên nếu sử dụng hàm khởi tạo mặc định này thì chúng ta sẽ gặp không ít phiền phức như:

    • Khi đổi thứ tự các stored property thì khi gọi hàm khởi tạo mặc định thứ tự của Parameter cũng phải đổi tương ứng
    struct Rectangle {
        var breadth: Double
        var length: Double
    }

    Khi gọi phải đảo lại parameter:

    let rect = Rectangle(breadth: 6, length: 12)
    • Khi thêm init với name các parameter khác với mặc định trong struct:
    struct Rectangle {
        var length: Double
        var breadth: Double
    
        init(length: Double) {
            self.length = length
            self.breadth = 5.0
        }
    }
    
    let rect = Rectangle(length: 6, breadth: 12) // Lỗi
    print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Đoạn code trên sẽ lỗi vì chúng ta đã viết 1 hàm init với parameter là length ở trong struct, việc này sẽ khiến cho swift không tự động viết hàm khởi tạo cho bạn nữa.
    Vậy làm thế nào để vừa có hàm khởi tạo tự viết và cả hàm khởi tạo tự động mà swift sẽ tạo cho chúng ta, đơn giản chỉ cần chuyển hàm init của chúng ta qua extension:

    struct Rectangle {
       var length: Double
       var breadth: Double
    }
    extension Rectangle {
       init(length: Double) {
           self.length = length
           self.breadth = length + 5.0
       }
    }

    Okey, vấn đề được giải quyết.

    3. Khởi tạo với các Parameter mặc định

    Đôi khi trong các hàm khởi tạo, bạn muốn gán giá trị mặc định cho các tham số như dưới đây:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        init(length: Double = 12.0, breadth: Double = 5.0) {
            self.length = length
            self.breadth = breadth
        }
    }
    
    let rectA = Rectangle()
    print(" rectA:\n length is: \(rectA.length)\n breadth is: \(rectA.breadth) ")
    
    let rectB = Rectangle(length: 50)
    print(" rectB:\n length is: \(rectB.length)\n breadth is: \(rectB.breadth) ")
    
    let rectC = Rectangle(length: 20, breadth: 6)
    print(" rectB:\n length is: \(rectC.length)\n breadth is: \(rectC.breadth) ")

    Kết quả là:

    rectA:
    length is: 12.0
    breadth is: 5.0
    rectB:
    length is: 50.0
    breadth is: 5.0
    rectB:
    length is: 20.0
    breadth is: 6.0

    Việc khởi tạo với các parameter mặc định khiến cho việc gọi hàm được đa dạng hơn, với tuỳ trường hợp thì sẽ có hay không có parameter.

    4. Initializer Delegation

    Initializer Delegation thực chất chính là việc gọi hàm khởi tạo từ các hàm khởi tạo khác, như ví dụ dưới đây:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        // 1
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    
        // 2
        init(length: Double) {
            let breadth = length + 2.0
            self.init(length: length, breadth: breadth)
        }
    
    }
    
    let rect = Rectangle(length: 50)
    print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Kết quả là:

    rect:
    length is: 50.0
    breadth is: 52.0

    Ở đoạn code trên thì chúng ta có tới 2 hàm init trong struct, ở hàm init thứ 2 thì chúng ta thực hiện tính toán bề rộng trước khi gọi đến hàm init 1.
    Đây chính là Initializer Delegation nhé, viết ra 1, 2 lần là quen tay ngay.

    Điều gì sẽ xảy ra khi chúng ta gọi self trước khi call hàm init số 1, sửa đoạn code như sau:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        // 1
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    
        // 2
        init(length: Double) {
            self.length = 5.0 // Báo lỗi: 'self' used before 'self.init' call or assignment to 'self'
    
            let breadth = length + 2.0
            self.init(length: length, breadth: breadth)
        }
    
    }
    
    let rect = Rectangle(length: 50)
    print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Lúc này thì compiler báo lỗi: ‘self’ used before ‘self.init’ call or assignment to ‘self’
    Lỗi này thông báo rằng chúng ta không được dùng self trước khi gọi Initializer Delegation. Việc này đảm bảo code sẽ không bị thay đổi không mong muốn. Giả sử như đoạn code trên pass qua compiler thì lúc này length = 50.0 chứ không phải bằng 5.0 như mong muốn.

    Chú ý trong phần I:

    - Khi khởi tạo 1 instance, chúng ta cần gán giá trị ban đầu cho tất cả các non-optional stored property.
    - Không gọi self trước khi call Initializer Delegation

    II. Khởi tạo trong Class

    Ở phần trên giới thiệu việc khởi tạo cũng như các vấn đề thường gặp khi khởi tạo 1 stucture. Đối với class thì việc khởi tạo sẽ tương đối khác và biến hoá hơn, biến hoá dư nào thì các bạn đọc ở bên dưới nhé:

    1. Hai loại khởi tạo trong class

    Riêng với class thì khởi tạo chia thành 2 loại là designated initializers and convenience initializers

    1.1. Designated Initializer

    Được coi là việc khởi tạo chính của class, đây là cách khởi tạo thông thường nhất. Trong hàm khởi tạo chúng ta cần gán giá trị ban đầu cho tất cả các non-optional stored property.

    class Rectangle {
        var length: Double
        var breadth: Double
    
        // Designated Initializer
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    }

    Khi class của bạn có stored property là non-opional thì Việc khởi tạo với kiểu Designated Initializer là bắt buộc.

    1.1. Convenience Initializers

    Được coi là hàm hỗ trợ việc khởi tạo của class, trong hàm khởi tạo này chúng ta sẽ gọi đến các hàm khởi tạo khác ( Khá giống với Initializer Delegation trong struct)

    class Rectangle {
        var length: Double
        var breadth: Double
    
        // Designated Initializer
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    
        // Convenience Initializer
        convenience init(length: Double, area: Double) {
            let breadth = area / length
            self.init(length: length, breadth: breadth)
    
        }
    }
    
    let rect = Rectangle(length: 5, area: 20)
    print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Kết quả sau khi chạy

    rect:
    length is: 5.0
    breadth is: 4.0

    Nhìn vào ví dụ trên thì chúng ta cũng thấy được vai trò của hàm Convenience Initializer rồi, khi bạn cần xử lý abc-xyz ở trong hàm khởi tạo, để việc khởi tạo gọi là tiện lợi nhất đối với bài toán của bạn,
    thì hãy nghĩ ngay đến Convenience Initializer nhé.

    2. Khởi tạo với subclass

    Để hiểu rõ phần này chúng ta đến với phần ví dụ luôn nhé:

    class Rectangle {
        var length: Double
        var breadth: Double
    
        // Designated Initializer
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    }
    
    class Square: Rectangle {
        var area: Double
    }

    Khi run đoạn code trên chúng ta sẽ nhận được lỗi là

    stored property 'area' without initial value prevents synthesized initializers
        var area: Double

    Như đã được phân tích ở phần I, khi khởi tạo 1 instance bất kỳ thì luôn phải đảm bảo các giá trị mặc định cho tất cả các stored property non-optional
    Ở đây do đã khởi tạo thêm biến area ở subclass là Square nên swift yêu cầu phải gán giá trị.

    Như dưới đây là 2 cách để pass qua compiler

    // option 1
    class Square: Rectangle {
        var color: String = "red"
    }
    
    // option 2
    class Square: Rectangle {
        var color: String
        init(length: Double, breadth: Double, color: String) {
            self.color = color
            super.init(length: length, breadth: breadth)
        }
    }

    Ở option 1, sẽ gán giá trị mặc định cho property color là “red”
    Ở option 2, thêm một hàm khởi tạo ở subclass Square để thực hiện Designated Initializer

    Vậy sẽ thế nào nếu chúng ta viết như sau:

    class Square: Rectangle {
        var color: String
        init(length: Double, breadth: Double, color: String) {
            self.color = color
            self.length = length
            self.breadth = breadth
        }
    }

    Lúc này ta nhận được error với nội dung:

    error: 'self' used in property access 'length' before 'super.init' call
            self.length = length

    Compiler đang báo chúng ta đang sử dụng self để truy cập vào property length trước khi gọi super.init.
    super ở đây chính là class cha Rectangle, việc sử dụng super.init là bắt buộc để gán giá trị cho lengt và breadth.

    Tóm lại thì việc khởi tạo của subclass cũng chung quy tắc là phải gán giá trị ban đầu cho tất cả các non-optional stored property, thứ tự thì sẽ là gán property ở subclass trước, sau đó là gọi super.init để gán cho các property ở lớp cha.

    3. Thừa kế hàm khởi tạo

    Ở subclass, nếu chúng ta muốn khởi tạo bằng cách gọi hàm khởi tạo của class cha thì bắt buộc subclass đó phải có giá trị mặc định của tất cả các non-optional stored property và subclass không có hàm init. Việc này khiến khá khó chịu, vì vậy để sử dụng hàm khởi tạo của class cha chúng ta có thể sử dụng thừa kế hàm khởi tạo như sau:

    class Square: Rectangle {
        var color: String
    
        init(length: Double, breadth: Double, color: String) {
            self.color = color
            super.init(length: length, breadth: breadth)
        }
    
        override init(length: Double, breadth: Double) {
            self.color = "red"
            super.init(length: length, breadth: breadth)
        }
    }

    Đơn giản chỉ cần override lại hàm init từ class cha là Rectangle, chúng ta có thể sài hàm khởi tạo của class cha một cách ngon lành rồi.

    III. Tổng kết

    Bài viết này mình đã giới thiệu về khởi tạo ở structure cũng như class, hy vọng sẽ giúp các bạn hiểu sâu hơn về khởi tạo trong swift.
    Phần sau mình sẽ giới thiệu về các cách khởi tạo của 1 custom class thuộc UI element nhé (ví dụ như UIView)
    Mong các bạn đón đọc.

  • Fresher Training—iOS Basic Day 2

    Fresher Training—iOS Basic Day 2

    Today topic:

    • App Life cycle
    • View Controller Life cycle
    • UIView

    Exerices:

    Exercise 01: App Life Cycle
    • Hãy phân tích những delegate sẽ được gọi trong những trường hợp sau:
      • Khi user quit app từ fast app switcher (multi task)
      • Khi app bị crash do source code
      • Khi app bị suspended
      • Khi user mở app khác (bằng cách tap vào notification của app khác hoặc open app khác từ app hiện tại)

    4 điểm

    Exercise 02: View Life Cycle

    • Hãy liệt kê những methods (delegate) được gọi khi
      • Push screen B từ screen A
      • Back lại screen A từ screen B
      • User tap button Home của iPhone để cho app xuống background rồi mở lại app.

    2 điểm

    Exercise 03: View

    • Hãy phân tích điểm khác nhau và giống nhau giữa frame và bounds. Nhất là trong trường hợp như ảnh.
      • Biết toạ độ của A(x: 10, y: 55)
      • Biết toạ độ của B(x: 60, y: 5)
      • Biết toạ độ của C(x: 110, y: 55)
      • Biết toạ độ của D(x: 60, y: 105)
      • AC vuông góc BD

    2 điểm

  • Fresher Training—iOS Swift Day 4

    Fresher Training—iOS Swift Day 4

    Today topic:

    • Encoding & Decoding Types
    • Asynchronous Closures & Memory Management
    • Value Types & Value Semantics
    • Protocol-Oriented Programming

    Tham khảo: https://nhathm.com/swift-closure-escaping-autoclosure-b6cc22729e7

    Exercises:

    Exercise 01: ENCODING & DECODING

    Make this source code Codeable

    struct Student {
        var name: String
        var age: Int
        var study: [StudyClass]
    }
    
    struct StudyClass {
        var className: String
        var classCode: String
    }

    Điểm: 1

    Exercise 02: ENCODING & DECODING

                Decoding this JSON

    [{

             “country”: {

                  “country_capital”: {

                      “capital_name”: “Ha Noi”,

                      “population”: 5000000

                  },

                  “country_name”: “Viet Nam”

             }

         },

         {

             “country”: {

                  “country_capital”: {

                      “capital_name”: “Tokyo”,

                      “population”: 4000000

                  },

                  “country_name”: “Japan”

             }

         }

    ]

    Điểm: 2

    Exercise 03: MEMORY MANAGEMENT

        What wrong with below code? Fix it

    class People {
        let name: String
        let email: String
        var bike: Bike?
        
        init(name: String, email: String) {
            self.name = name
            self.email = email
        }
        
        deinit {
            print("People deinit \(name)!")
        }
    }
    
    class Bike {
        let id: Int
        let type: String
        var owner: People?
        
        init(id: Int, type: String) {
            self.id = id
            self.type = type
        }
        
        deinit {
            print("Bike deinit \(type)!")
        }
    }
    
    var owner: People? = People(name: "NhatHM", email: "[email protected]")
    var bike: Bike? = Bike(id: 1, type: "Honda")
    
    owner?.bike = bike
    bike?.owner = owner
    
    owner = nil
    bike = nil

    Điểm: 2

    Exercise 04: PROTOCOL

    Making this source code runable

    var color = UIColor.aliceBlue
    color = UIColor.oceanBlue
    

    Điểm: 2

    Exercise 05: generics

    What is generics? Show me an example

    Điểm: 1

  • Fresher Training—iOS Swift Day 3

    Fresher Training—iOS Swift Day 3

    Today topic:

    • Access Control & Code Organization
    • Custom Operators, Subscripts & Keypaths
    • Pattern Matching
    • Error Handling

    Thao khảo: Swift—Advanced control flow

    Exercises:

    Exercise 01: SINGLETON
    • A singleton is a design pattern that restricts the instantiation of a class to one object.
    • Use access modifiers to create a singleton class Logger. This Logger should:
    • Provide shared, public, global access to the single Logger object.
    • Not be able to be instantiated by consuming code.
    • Have a method log() that will print a string to the console.

    Điểm: 2

    Exercise 02: STACK
    • Declare a generic type Stack. A stack is a LIFO (last-in-first-out) data structure that supports the following operations:
    • peek: returns the top element on the stack without removing it. Returns nil if the stack is empty.
    • push: adds an element on top of the stack.
    • pop: returns and removes the top element on the stack. Returns nil if the stack is empty.
    • count: returns the size of the stack.
    • Ensure that these operations are the only exposed interface. In other words, additional properties or methods needed to implement the type should not be visible.

    Điểm: 2.5

    Exercise 03: SUBSCRIPT
    extension Array {
        subscript(index: Int) -> (String, String)? {
            guard let value = self[index] as? Int else {
                return nil
            }
            
            switch (value >= 0, abs(value) % 2) {
            case (true, 0):
                return ("positive", "even")
            case (true, 1):
                return ("positive", "odd")
            case (false, 0):
                return ("negative", "even")
            case (false, 1):
                return ("negative", "odd")
            default:
                return nil
            }
        }
    }

    What wrong with this code? How to fix?

    Điểm: 1.5

    Exercise 04: ERROR HANDLING

    Write a throwing function that converts a String to an even number, rounding down if necessary.

    Điểm: 2

  • Swift—Advanced control flow

    Swift—Advanced control flow

    Bài viết này giới thiếu cái khái niệm và các dùng về control flow trong Swift:

    For loop

    Countable ranges

    • countable closed range: 0…5
    • countable half-open range: let halfOpenRange = 0..<5

    For in với where condition

    • Swift hỗ trợ for in where để lọc ra các điều kiện phù hợp trong tập cho trước:
    var sum = 0
    
    for i in 1...10 where i % 2 == 1 {
        sum += i
    }

    Continue and labeled statements

    • Trong nhiều trường hợp, khi điều kiện trong vòng loops trở nên phức tạp hơn, thì ta có thể sử dụng “labeled statements” để tiếp tục vòng loop tương ứng, ví dụ như dưới:
    var sum = 0
    rowLoop: for row in 0..<8 {
        columnLoop: for column in 0..<8 {
            if row == column {
                continue rowLoop
            }
            sum += row * column
            print(sum)
        }
    }
    • Ở đây, rowLoop và columnLoop được gọi là “labeled statements”, ta có thể tiếp tục vòng loop tương ứng bằng câu lệnh “continue”

    Switch statements

    • Không như Objective C là switch điều kiện switch (expression) chỉ có thể sử dụng các loại data dạng như int, character…, thì trong Swift, điều kiện switch đã trở nên phong phú và thuận tiện hơn rất nhiều.
    • Các điều kiện switch mà Swift support:
      • Điều kiện switch là một hoặc nhiều giá trị cùng loại data với nhau
      • Điều kiện switch có thể là range
    • Các điểm chính của Switch Statement:
      • No implicit Fallthrough (không tự động chuyển sang case tiếp theo): đối với các case điều kiện của Switch statement thì không có chuyện điều kiện switch được tự động chuyển sang case tiếp theo, nghĩa là mỗi điều kiện trong case của switch đều phải có body. Đoạn code như dưới sẽ báo lỗi khi compile. (Trong trường hợp muốn kết hợp nhiều điều kiện switch case thì sử dụng “compound case” switch.)
    switch age {
    case 19: // Error
    case 20:
        print("adult")
    default:
        print("default case")
    }
    • Interval matching: switch trong Swift hỗ trợ switch case theo từng khoảng giá trị. Ví dụ:
    let age = 18
    
    switch age {
    case 0..<18:
        print("child")
    case 18..<60:
        print("adult")
    default:
        print("old")
    }
    • Tuples: Swift switch statement hỗ trợ điều kiện trong switch case là tuple, cách sử dụng cũng rất linh hoạt. Ví dụ (lấy từ swift.org):
    let somePoint = (1, 1)
    switch somePoint {
    case (0, 0):
        print("\(somePoint) is at the origin")
    case (_, 0):
        print("\(somePoint) is on the x-axis")
    case (0, _):
        print("\(somePoint) is on the y-axis")
    case (-2...2, -2...2):
        print("\(somePoint) is inside the box")
    default:
        print("\(somePoint) is outside of the box")
    }
    • Đối với trường hợp sử dụng tuple thì điều kiện trong case có thể là mapping cả tuple, hoặc chỉ 1 giá trị trong tuple mà thôi, đối với giá trị không cần so sánh thì dùng ký tự “_” (wildcard pattern) để định nghĩa.
    • Value Bindings: trong trường hợp muốn lấy giá trị của giá trị trong tuple khi đã thoả mãn điều kiện thì dùng câu lệnh như sau:
    let anotherPoint = (2, 0)
    switch anotherPoint {
    case (let x, 0):
        print("on the x-axis with an x value of \(x)")
    case (0, let y):
        print("on the y-axis with a y value of \(y)")
    case let (x, y):
        print("somewhere else at (\(x), \(y))")
    }
    • -> như trong trường hợp này thì chỉ cần giá trị point thoả mãn y = 0 thì sẽ lấy được giá trị của x ra bằng câu lệnh “let x”
    • Where: where statement được dùng trong switch case để xử lý những câu switch với điều kiện phức tạp, ví dụ:
    let person = ("Gaby", "Female", 18)
    switch person {
    case (_, _, let age) where (age > 50):
        print("This person is not in age range")
    case (_, let gender, _) where (gender == "Male"):
        print("This is male employee")
    case (_, let gender, _) where (gender == "Female"):
        print("This is female employee")
    default:
        print("Default")
    }
    • Compound case: trong trường hợp nhiều điều kiện case xử lý chung một logic thì ta có thể kết hợp các điều kiện đó vào chung 1 case switch, ví dụ:
    let language = "Swift">switch language {
    case "Swift", "Objective-C":
        print("Company: Apple")
    case "Dart", "Go lang":
        print("Company: Google")
    default:
        print("Some other companies")
    }
    • Switch statement cho phép sau khi xử lý logic ở case trên, sẽ tiếp tục xử lý logic ở case dưới với keyword “fallthrough”

    Control Transfer Statements

    • continue:
      • Câu lệnh continue hỗ trợ việc break ra khỏi vòng lặp hiện tại của for-loop mà không break hoàn toàn khỏi for-loop
    • break
      • Câu lệnh break trong loop statement sẽ ngắt hoàn toàn vòng lặp.
      • Câu lệnh break trong switch statement sẽ kết thúc switch statement. Bởi vì các case của switch statement không được phép empty nên thường add break vào để tránh compile error. Ví dụ add break vào default case.
    • fallthrough
      • Fallthrough cho phép switch statement sau khi thực hiện xong logic của case bất kỳ, sẽ được chuyển tiếp xuống dưới để thực hiện case tiếp theo. Thường ứng dụng trong trường hợp muốn append thêm logic vào kết quả đã được xử lý ở các case của switch statement (sẽ viết ở default case)
    • return
    • throw
      • https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID510
  • Fresher Training—iOS Swift Day 2

    Fresher Training—iOS Swift Day 2

    Today topic:

    • Properties
    • Methods
    • Advanced Classes Enumerations
    • Protocols
    • Generics

    Exercises:

    Exercise 01: PROPERTY
    1. Show me an example about a struct have property is another struct
    2. What is type property, example by source code

    1 and 1 point

    Exercise 02: Methods
    1. Define struct Coordinate with property have latitude and longitude. Write a method to get/set for this property
    2. (Optional) Compare between class’s method and struct’s method

    1 and 0.5 point

    Exercise 03: Advanced class

    What is deinit of class?

    Create three simple classes called A, B, and C where C inherits from B and B inherits from A.

    Write init method that print class name before and after call super.init

    Write deinit method then printed simple log like: Destroy A / B / C

    Now, look at below codes

    do {
        let _ = C()
    }

    These codes will create an instace of C then release. Please explain printed log

    2 point

    Exercise 04: Protocols

    Define a protocol Vehical, this protocol will have some properties like fuel, speed, and some method like run()…

    Implement at least 3 class conform to this protocol and show me difference kind of run()

    What do you think about inheritance and protocol for this case?

    2.5 point

  • Fresher Training—iOS Swift Day 1

    Fresher Training—iOS Swift Day 1

    Today topic:

    • Expressions, Variables & Constants
    • Types & Operations
    • Control Flow
    • Functions
    • Optionals
    • Arrays, Dictionaries & Sets
    • Collection Iteration with Closures
    • Strings
    • Structures
    • Classes

    Exerices:

    Exercise 01: Expressions, Variables & Constants

    Declare four constants named x1, y1, x2 and y2 of type Double. These constants represent the 2-dimensional coordinates of two points. Calculate the distance between these two points and store the result in a constant named distance.

    1 point

    Exercise 02: Expressions, Variables & Constants

    Given a number n, calculate the factorial of n.  (Example: 4 factorial is equal to 1 * 2 * 3 * 4.)

    1 point

    Exercise 03: Control Flow
    1. Print 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0. Don’t use stride(from:by:to:)
    2. What is Labeled statements? Using source code to explain.

    1 point and 1.5 point

    Exercise 03: Control Flow
    1. Write a function that print log. This function can take a string as parameter, or multiple strings as paramsters. Return false if any string is empty.
    2. (Optional) Write a function that calculate fibonancy sequence:  func fibonacci(_ number: Int) -> Int

    1 point and bonus 0.5 point

    Exercise 04: Collection Iteration with Closures

    Define a dictionary with key is name: String and value is score: Double
    Calculate sum of all score in dictionary

    1 point

    Exercise 05: Struct vs Class

    Compare between struct and class

    1.5 point

  • [Android] Phân tích và mô phỏng nút cảm xúc của Android Facebook Application

    [Android] Phân tích và mô phỏng nút cảm xúc của Android Facebook Application

    Video demo:

    Tình hình là đợt vừa rồi mình có ngó Kiaplog profile của anh Huy Trần, lướt lướt thấy có chủ đề Phức tạp hoá vấn đề: Phân tích và mô phỏng nút cảm xúc của Facebook có lượng kipalog khiếp quá nên nhảy vào xem luôn. Đọc xong mà thấy mở mang đầu óc, nhưng tiếc là lâu chưa xem lại web + cũng gà nữa nên chắc chả code theo được :disappointed_relieved: , đành ngậm ngùi hấp thu phân tích của anh + phân tích thêm để giống với app Facebook và nung nấu chuyển hóa nó sang Android :sunglasses:

    Biểu tượng cảm xúc mới của Facebook trên Android

    Đầu tiên khi mình nhìn vào reaction box (hộp biểu tượng) trên web và tư duy theo cách thiết kế trên Android thì tặc lưỡi “không khó lắm nhỉ, chắc dùng mấy view con trong layout rồi mông má thêm tí animation là oke”. Nói thế chứ cũng phải kiểm chứng lại trong app, không lại “treo đầu dê bán thịt chó”.

    Đầu tiên là install app :sweat_smile: (mình gỡ khá lâu rồi vì nó nofity liên tọi). Long click thử vào nút Like nào… Má ơi! :scream: Các chuyển động + kích thước khá là khác với web, bỗng nhiên nghi ngờ xem thằng facebook nó làm gì với view đó nên liền bật ngay bounds lên để xem (Settings > Developer options > Show layout bounds) thì thôi xong, đây là kết quả: :sob:

    Không có 1 cái viền nào xung quanh cái reaction box > Nó vẽ lên view chứ mếu phải dùng layout (Đường viền ngoài là viền của cả cái view reaction) :sob:. Rồi luôn, vẽ thì vẽ, hồi bé thích vẽ lắm, cứ tưởng là lớn lên làm kiến trúc sư cơ đấy :joy:

    Phân tích hiệu ứng Reaction

    (Phần này sẽ có những phần lấy từ bài anh Huy Trần, chỉ nhằm mục đích tiện cho mọi người theo dõi)

    Những phần dưới đây là thiết kế cho web, những phần khác so với mobile mình sẽ chỉ rõ sau. Đầu tiên là một bản tin trên newfeed mà chúng ta thường thấy:

    Tiếp theo là khi chúng ta nhấn lâu (long click) vào nút like, reactions box sẽ xuất hiện theo hướng từ dưới lên + từ mờ thành rõ dần:

    Tiếp theo ngay sau đó là các emotion xuất hiện, chúng liên tiếp xuất hiện theo hướng từ dưới lên + từ mờ thành rõ dần (alpha tăng) + từ bé thành lớn dần (size tăng):

    Chúng ta có thể giả sử rằng tất cả các thành phần như reactions box + emotion đều thực hiện chuyển động của chúng trong 0.3s, nhưng thời điểm bắt đầu của chúng sẽ khác nhau như: reactions box (xuất phát lúc 0.0s), emotion 1 (xuất phát lúc 0.1s), emotion 2 (xuất phát lúc 0.2s), …tương tự với các emotion tiếp theo.

    Nếu phân tích kĩ hơn hiệu ứng di chuyển từ dưới lên trên của các emotion thì các emotion sẽ di chuyển như sau:

    Chú ý: Hình vẽ trên chỉ thể hiện trạng thái di chuyển theo chiều dọc (tức trục Oy) và trục Ox chính là thời gian thực hiện.

    Ở vị trí đầu tiên xuất hiện, emotion sẽ mờ + cách xa reactions box, chúng di chuyển dần dần đến vị trí của chúng ở reactions box, nhưng chúng sẽ đi quá thêm 1 đoạn nhỏ sau đó quay trở lại vị trí của chúng ở reactions box (đừng quá lo lắng về cách xử lý, nó đơn giản chỉ là 1 phương trình xy thui :relaxed:)

    Sau khi hoàn thành hiệu ứng, chúng ở trạng thái “bình thường” như hình dưới đây:

    Đến đây có lẽ chúng ta cần dừng lại một chút để phân tích thêm việc khi di chuyển các thành phần đối với ứng dụng Facebook trên Android. Khi ta di tay vào emotion:

    • Chiều cao của reactions box nhỏ lại, tuy nhiên độ rộng vẫn giữ nguyên.
    • Các emotion không được select sẽ nhỏ lại.
    • Emotion được select sẽ to ra (gấp khoảng 2.5 đến 3 lần gì đó).
    • Title của emotion xuất hiện phía trên emotion + hiệu ứng bé thành lớn dần + mờ thành rõ dần.
    • Khi ngón tay di chuyển ra khỏi cả view reaction thì các thành phần trở lại trạng thái “bình thường”.

    Móe, cứ tưởng được làm như web ==’ ai ngờ lại thêm mấy thứ này, khó nhằn phết nhưng thui cứ chiến nhỉ?

    À một tí quan điểm trước khi code 😀 :

    • Những ý tưởng + logic dưới đây hoàn toàn là ý kiến cá nhân của mình, có thể chưa hợp lý > mong mọi người đóng góp.
    • Code nhắm đến mục đích mô phỏng chứ không nhắm đến viết thư viện > Đừng quở trách “thằng này code đụt, chả flexible gì cả” tội em :'(
    • Mình đã cố gắng để cho em nó “mượt” đến mức có thể, do thời gian có hạn và chắc hẳn là cũng khó để mượt như Facebook :sweat:

    Trạng thái

    Theo mình phân tích thì để diễn tả tất cả các hành động của reaction thì gồm có 4 trạng thái:

    1. Trạng thái “BEGIN” – là trạng thái các thành phần lúc bắt xuất hiện.
    2. Trạng thái “NORMAL” – là trạng thái các emotion kích thước như nhau, nằm ngay ngắn trong box.
    3. Trạng thái “CHOOSING” – là trạng thái emotion được chọn phóng to, emotion còn lại + box thu nhỏ lại.
    4. Trạng thái “CHOOSED” (từ này không có trong TA thì phải :joy:) – là trạng thái emotion đc chọn sẽ bắn vút lên, các emotion còn lại sẽ sụp xuống và biến mất hoàn toàn.

    Trong phạm vi bài viết này mình sẽ trình bày 3 trạng thái đầu, trạng thái thứ 4 anh em tự chém thêm nhé :kissing_closed_eyes:

    Hiển thị trạng thái “NORMAL”

    Trạng thái này gồm có Board (Reaction Box) và 6 Emotion (Emotion Images Download)

    Reaction View

    Tạo một class ReactionView và extends từ View:

    public class ReactionView extends View {
    
      enum StateDraw {
          BEGIN,
          CHOOSING,
          END
      }
    
      public static final long DURATION_ANIMATION = 200;
    
      public static final long DURATION_BEGINNING_EACH_ITEM = 300;
    
      public static final long DURATION_BEGINNING_ANIMATION = 900;
    
      private Board board;
    
      private Emotion[] emotions = new Emotion[6];
    
      private StateDraw state = StateDraw.BEGIN;
    
      private int currentPosition = 0;
    
      public ReactionView(Context context) {
          super(context);
          init();
      }
    
      public ReactionView(Context context, AttributeSet attrs) {
          super(context, attrs);
          init();
      }
    
      public ReactionView(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init();
      }
    
      private void init() {
    
      }
    
      private void initElement() {
    
      }
    
      @Override
      protected void onDraw(Canvas canvas) {
    
      }
    
      private void beforeAnimateBeginning() {
    
      }
    
      private void beforeAnimateChoosing() {
    
      }
    
      private void beforeAnimateNormalBack() {
    
      }
    
      private void calculateInSessionChoosingAndEnding(float interpolatedTime) {
    
      }
    
      private void calculateInSessionBeginning(float interpolatedTime) {
    
      }
    
      private int calculateSize(int position, float interpolatedTime) {
          return 0;
      }
    
      private void calculateCoordinateX() {
    
      }
    
      public void show() {
    
      }
    
      private void selected(int position) {
    
      }
    
      public void backToNormal() {
    
      }
    
      @Override
      public boolean onTouchEvent(MotionEvent event) {
    
          return true;
      }
    
      class ChooseEmotionAnimation extends Animation {
          public ChooseEmotionAnimation() {
    
          }
    
          @Override
          protected void applyTransformation(float interpolatedTime, Transformation t) {
    
          }
      }
    
      class BeginningAnimation extends Animation {
    
          public BeginningAnimation() {
    
          }
    
          @Override
          protected void applyTransformation(float interpolatedTime, Transformation t) {
    
          }
      }
    }

    Giờ thì thêm view này vào 1 activity để chúng ta cùng vẽ “tha thu” lên nha :sunglasses:, mình thêm luôn vào activity_main.xml đi:

        <?xml version="1.0" encoding="utf-8"?>
        <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/activity_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.hado.facebookemotion.MainActivity">
    
            <com.hado.facebookemotion.ReactionView
                android:id="@+id/view_reaction"
                android:layout_marginLeft="20dp"
                android:layout_width="@dimen/width_view_reaction"
                android:layout_height="@dimen/height_view_reaction" />
    
            <Button
                android:id="@+id/btn_like"
                android:layout_width="100dp"
                android:layout_height="50dp"
                android:layout_below="@+id/view_reaction"
                android:text="Like" />
        </RelativeLayout>
    • Dimen width_view_reaction = 300dp
    • Dimen height_view_reaction = 250dp

    Board & Emotion

    • Độ cao (height): 50dp
    • Độ rộng (width): 275dp (6 emotion 40dp + khoảng cách giữa emotion vs nhau và với cạnh trái phải board là 5dp => 7*5 = 35dp)
    • Baseline là đường thẳng cố định, không thay đổi để giúp các emotion được thẳng hàng. Công thức: tọa độ BASE_LINE = BOARD_Y + Emotion.NORMAL_SIZE + DIVIDE

    Đầu tiên ta tạo vài lớp dùng chung đã nhé:

    Lớp Util:

    public class Util {
        public static int dpToPx(int dp) {
            return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
        }
    }

    Lớp CommonDimen:

    public class CommonDimen {
        public static int DIVIDE = Util.dpToPx(5);
    
        public static int HEIGHT_VIEW_REACTION = Util.dpToPx(250);
    
        public static int WIDHT_VIEW_REACTION = Util.dpToPx(300);
    
        public static final int MAX_ALPHA = 255;
    
        public static final int MIN_ALPHA = 150;
    }

    Oke, bây giờ ta tạo lớp cho các đối tượng ta cần vẽ. Class Emotion:

    public class Emotion {
        private Context context;
    
        public static final int MINIMAL_SIZE = Util.dpToPx(28);
    
        public static final int NORMAL_SIZE = Util.dpToPx(40);
    
        public static final int CHOOSE_SIZE = Util.dpToPx(100);
    
        public static final int DISTANCE = Util.dpToPx(15);
    
        public static final int MAX_WIDTH_TITLE = Util.dpToPx(70);
    
        public int currentSize = NORMAL_SIZE;
    
        public int beginSize;
    
        public int endSize;
    
        public float currentX;
    
        public float currentY;
    
        public float beginY;
    
        public float endY;
    
        public Bitmap imageOrigin;
    
        public Bitmap imageTitle;
    
        public Paint emotionPaint;
    
        public Paint titlePaint;
    
        private float ratioWH;
    
    
        public Emotion(Context context, String title, int imageResource) {
            this.context = context;
    
            imageOrigin = BitmapFactory.decodeResource(context.getResources(), imageResource);
    
            emotionPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
            emotionPaint.setAntiAlias(true);
    
            titlePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
            titlePaint.setAntiAlias(true);
    
            generateTitleView(title);
        }
    
        private void generateTitleView(String title) {
    
        }
    
        public void setAlphaTitle(int alpha) {
            titlePaint.setAlpha(alpha);
        }
    
        public void drawEmotion(Canvas canvas) {
            canvas.drawBitmap(imageOrigin, null, new RectF(currentX, currentY, currentX + currentSize, currentY + currentSize), emotionPaint);
            drawTitle(canvas);
        }
    
        public void drawTitle(Canvas canvas) {
    
        }
    }

    Class Board:

    public class Board {
    
        public static final int BOARD_WIDTH = 6 * Emotion.NORMAL_SIZE + 7 * CommonDimen.DIVIDE; //DIVIDE = 5dp, Emotion.NORMAL_SIZE = 40dp
    
        public static final int BOARD_HEIGHT_NORMAL = Util.dpToPx(50);
    
        public static final int BOARD_HEIGHT_MINIMAL = Util.dpToPx(38);
    
        public static final float BOARD_X = 10;
    
        public static final float BOARD_BOTTOM = CommonDimen.HEIGHT_VIEW_REACTION - 200;
    
        public static final float BOARD_Y = BOARD_BOTTOM - BOARD_HEIGHT_NORMAL;
    
        public static final float BASE_LINE = BOARD_Y + Emotion.NORMAL_SIZE + CommonDimen.DIVIDE;
    
        public Paint boardPaint;
    
        public float currentHeight = BOARD_HEIGHT_NORMAL;
    
        public float currentY = BOARD_Y;
    
        public float beginHeight;
    
        public float endHeight;
    
        public float beginY;
    
        public float endY;
    
    
        public Board(Context context) {
            initPaint(context);
        }
    
        private void initPaint(Context context) {
            boardPaint = new Paint();
            boardPaint.setAntiAlias(true);
            boardPaint.setStyle(Paint.Style.FILL);
            boardPaint.setColor(context.getResources().getColor(R.color.board));
            boardPaint.setShadowLayer(5.0f, 0.0f, 2.0f, 0xFF000000);
        }
    
        public void setCurrentHeight(float newHeight) {
            currentHeight = newHeight;
            currentY = BOARD_BOTTOM - currentHeight;
        }
    
        public float getCurrentHeight() {
            return currentHeight;
        }
    
        public void drawBoard(Canvas canvas) {
            float radius = currentHeight / 2;
            RectF board = new RectF(BOARD_X, currentY, BOARD_X + BOARD_WIDTH, currentY + currentHeight);
            canvas.drawRoundRect(board, radius, radius, boardPaint);
        }
    }

    Giờ thì quay lại ReactionView để vẽ thử board và các emotion lên xem thế nào nhé. Trước tiên ta khởi tạo đối tượng cho các thành phần:
    Method init():

    private void init() {
        board = new Board(getContext());
        setLayerType(LAYER_TYPE_SOFTWARE, board.boardPaint);
    
        emotions[0] = new Emotion(getContext(), "Like", R.drawable.like);
        emotions[1] = new Emotion(getContext(), "Love", R.drawable.love);
        emotions[2] = new Emotion(getContext(), "Haha", R.drawable.haha);
        emotions[3] = new Emotion(getContext(), "Wow", R.drawable.wow);
        emotions[4] = new Emotion(getContext(), "Cry", R.drawable.cry);
        emotions[5] = new Emotion(getContext(), "Angry", R.drawable.angry);
    
        //BEGIN: Đoạn này để đặt các thành phần vào vị trí ban đầu để xem kết quả thui,
        //chứ các thành phần ban đầu sẽ bị ẩn đi, vì chưa click like mà :D
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
            emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
        }
        //END
    
        initElement();
    }

    Cùng xem lại hình này nhé:

    • Trường hợp này tọa độ Y của tất cả các emotion sẽ bằng nhau, tọa độ Y sẽ nằm ở góc trái phía trên các emotion => currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE
    • Còn tọa độ X của các emotion sẽ được tính dựa trên 2 trường hợp: Nếu nó là emotion đầu tiên, thì nó luôn bằng tọa độ X của bảng + thêm khoảng cách nhỏ. Còn các emotion còn lại sẽ bằng tọa độ X thằng đứng trước + kích thước hiện tại của thằng đứng trước (size) + thêm khoảng cách nhỏ.

    Method onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
        board.drawBoard(canvas);
        for (Emotion emotion : emotions) {
            emotion.drawEmotion(canvas);
        }
    }

    Ở method onDraw này sẽ thực hiện vẽ các đối tượng đã được tính sẵn kích thước và tọa độ ở hàm bên trên lên view. Oke, run cái nào :smiling_imp:

    Đây là kết quả hiện tại của chúng ta:

    :boom: Vậy là đã vẽ thành công các thành phần lên view, bây giờ để thực hiện các chuyển động khác thì ta chỉ cần tính toán lại kích thước + tọa độ rồi gọi method onDraw qua method invalidate() là các thành phần sẽ được cập nhật theo kích thước + tọa độ mới.

    Trạng thái “CHOOSING”

    Ở trạng thái này chúng ta phải thực hiện 3 công việc sau:

    1. Xử lý độ cao của reaction box giảm dần.
    2. Xử lý kích thước + tọa độ của các emotion.
    3. Xử lý kích thước + tọa độ của title emotion được chọn.

    Ta Override lại phương thức onTouchEvent để xác định được emotion nào đang được chọn:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = false;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handled = true;
                break;
            case MotionEvent.ACTION_MOVE:
                for (int i = 0; i < emotions.length; i++) {
                    if (event.getX() > emotions[i].currentX && event.getX() < emotions[i].currentX + emotions[i].currentSize) {
                        selected(i);
                        break;
                    }
                }
                handled = true;
                break;
            case MotionEvent.ACTION_UP:
                backToNormal();
                handled = true;
                break;
        }
        return handled;
    }
    • Khi ngón tay di chuyển trên màn hình, thì sẽ xác định xem tọa độ X của ngón tay đang nằm trong khoảng giá trị tọa độ X của emotion nào thì gọi method selected để thực hiện chuyển động phóng to emotion đó.
    • Khi ngon tay nhấc lên thì gọi method backToNormal để trở về trạng thái NORMAL.

    Method selected:

    private void selected(int position) {
        if (currentPosition == position && state == StateDraw.CHOOSING) return;
    
        state = StateDraw.CHOOSING;
        currentPosition = position;
    
        startAnimation(new ChooseEmotionAnimation());
    }

    Method backToNormal:

    public void backToNormal() {
        state = StateDraw.NORMAL;
        startAnimation(new ChooseEmotionAnimation());
    }

    Ta cần một chút animation để cho các chuyển động “nuột” hơn. Ở class ReactionView có khởi tạo class ChooseEmotionAnimation:

    class ChooseEmotionAnimation extends Animation {
        public ChooseEmotionAnimation() {
            if (state == StateDraw.CHOOSING) {
                beforeAnimateChoosing();
            } else if (state == StateDraw.NORMAL) {
                beforeAnimateNormalBack();
            }
            setDuration(DURATION_ANIMATION);
        }
    
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            calculateInSessionChoosingAndEnding(interpolatedTime);
        }
    }

    Vì ở trạng thái được chọn CHOOSING và trạng thái NORMAL (chuyển về từ trạng thái CHOOSING) có chung một cách xử lý nên sẽ dùng chung Animation và phương thức tính toán.

    Method beforeAnimateChoosing:

    private void beforeAnimateChoosing() {
        board.beginHeight = board.getCurrentHeight();
        board.endHeight = Board.BOARD_HEIGHT_MINIMAL;
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].beginSize = emotions[i].currentSize;
    
            if (i == currentPosition) {
                emotions[i].endSize = Emotion.CHOOSE_SIZE;
            } else {
                emotions[i].endSize = Emotion.MINIMAL_SIZE;
            }
        }
    }

    Method beforeAnimateNormalBack:

    private void beforeAnimateNormalBack() {
        board.beginHeight = board.getCurrentHeight();
        board.endHeight = Board.BOARD_HEIGHT_NORMAL;
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].beginSize = emotions[i].currentSize;
            emotions[i].endSize = Emotion.NORMAL_SIZE;
        }
    }

    Phương thức này dùng để xác định trạng thái hiện tại trước khi bắt đầu di chuyển như board.beginHeightboard.endHeight sẽ là di chuyển độ cao của bảng từ beginHeight đến endHeight trong khoảng thời gian DURATION_ANIMATION. Tương tự với các emotion cũng vậy.

    Method calculateInSessionChoosingAndEnding:

    private void calculateInSessionChoosingAndEnding(float interpolatedTime) {
        board.setCurrentHeight(board.beginHeight + (int) (interpolatedTime * (board.endHeight - board.beginHeight)));
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].currentSize = calculateSize(i, interpolatedTime);
            emotions[i].currentY = Board.BASE_LINE - emotions[i].currentSize;
        }
        calculateCoordinateX();
        invalidate();
    }

    Phương thức này sẽ được gọi liên tục trong lúc thực hiện animation để cập nhật view. Thường thì Animation sẽ thực hiện 60fms(frame/s), mỗi lần gọi đến phương thức này sẽ coi là 1 frame, việc của chúng ta là phải tính toán xem các thành phần đó đang ở kích thước + tọa độ nào trong thời điểm interpolatedTime đó (giá trị interpolatedTime là [0, 1] trong khoảng DURATION_ANIMATION).

    Method calculateSize:

    private int calculateSize(int position, float interpolatedTime) {
        int changeSize = emotions[position].endSize - emotions[position].beginSize;
        return emotions[position].beginSize + (int) (interpolatedTime * changeSize);
    }

    Phương thức này trả về size hiện tại của các emotion được tính theo interpolatedTime.

    Method calculateCoordinateX:

    private void calculateCoordinateX() {
        emotions[0].currentX = Board.BOARD_X + DIVIDE;
        emotions[emotions.length - 1].currentX = Board.BOARD_X + Board.BOARD_WIDTH - DIVIDE - emotions[emotions.length - 1].currentSize;
    
        for (int i = 1; i < currentPosition; i++) {
            emotions[i].currentX = emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
        }
    
        for (int i = emotions.length - 2; i > currentPosition; i--) {
            emotions[i].currentX = emotions[i + 1].currentX - emotions[i].currentSize - DIVIDE;
        }
    
        if (currentPosition != 0 && currentPosition != emotions.length - 1) {
            if (currentPosition <= (emotions.length / 2 - 1)) {
                emotions[currentPosition].currentX = emotions[currentPosition - 1].currentX + emotions[currentPosition - 1].currentSize + DIVIDE;
            } else {
                emotions[currentPosition].currentX = emotions[currentPosition + 1].currentX - emotions[currentPosition].currentSize - DIVIDE;
            }
        }
    }

    Method này thực hiện tính tọa độ X cho các emotion, tọa độ Y ở phương thức calculateInSessionChoosingAndEnding đã tính rùi. Như mình demo khi vẽ các emotion ở trạng thái ban đầu. Mình để sự rằng buộc tọa độ X như sau:

    Ví dụ: Ta có 6 emotions 1 2 3 4 5 6. Tọa độ X1 luôn luôn cố định, vì nó nằm bên cạnh của bảng. Tọa độ X2 phụ thuộc vào Tọa độ X1 + Size 1, Tọa độ X3 phụ thuộc vào Tọa độ X2 + Size 2, …Như vậy nếu vẽ tĩnh như lúc khởi tạo thì không vấn đề gì, nhưng khi di chuyển cùng với Animation, mọi thứ cập nhật liên tục khiến cho sự phụ thuộc về Tọa độ X + Size của các emotion cuối như 4 5 6 tăng lên làm các emotion di chuyển sai số + không mượt.

    => Giải pháp của mình được thể hiện ở đoạn code trên, nhằm giảm bớt sự phụ thuộc. Mình nhận thấy emotion 1 và 6 có tọa độ ổn định và không bị phụ thuộc nên mình sẽ lấy 2 emotion này làm chốt, từ đó emotion 2 3 sẽ phụ thuộc và 1, emotion 4 5 sẽ phụ thuộc vào 6. Kết quả là các emotion di chuyển khá mượt + chính xác.

    Oki, nói nhiều quá, nếu anh em đã implement xong các đoạn code bên trên thì run nào, đây là kết quả sẽ đạt được:

    Hề hế, gần xong phase này rùi đó, còn mỗi đồng chí title nữa thui. Giờ thì quay lại class Emotion một chút nào. Đầu tiên mình lại định dùng canvas vẽ tiếp text vs background của nó, nhưng thui thấy nhọc quá. Thế là làm 1 cái layout xong decode nó sang bitmap vẽ cho lẹ:

    Background XML background_tv_reaction:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="#80000000" />
        <corners android:radius="12.5dp" />
        <padding
            android:bottom="2dp"
            android:top="2dp" />
    </shape>

    Layout XML title_view:

    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="@dimen/width_title"
        android:layout_height="@dimen/height_title"
        android:background="@drawable/background_tv_reaction"
        android:gravity="center"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textColor="@android:color/white"></TextView>

    Class Emotion, Method generateTitleView:

    private void generateTitleView(String title) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View titleView = inflater.inflate(R.layout.title_view, null);
        ((TextView) titleView).setText(title);
    
        int w = (int) context.getResources().getDimension(R.dimen.width_title);
        int h = (int) context.getResources().getDimension(R.dimen.height_title);
        ratioWH = (w * 1.0f) / (h * 1.0f);
        imageTitle = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(imageTitle);
        titleView.layout(0, 0, w, h);
        ((TextView) titleView).getPaint().setAntiAlias(true);
        titleView.draw(c);
    }
    • Dimen width_title : 60dp
    • Dimen height_title : 25dp

    Method này mình tạo bitmap của titleView với string title tương ứng.

    Method drawTitle:

    public void drawTitle(Canvas canvas) {
        int width = (currentSize - NORMAL_SIZE) * 7 / 6;
        int height = (int) (width / ratioWH);
    
        setAlphaTitle(Math.min(CommonDimen.MAX_ALPHA * width / MAX_WIDTH_TITLE, CommonDimen.MAX_ALPHA));
    
        if (width <= 0 || height <= 0) return;
    
        float x = currentX + (currentSize - width) / 2;
        float y = currentY - DISTANCE - height;
    
        canvas.drawBitmap(imageTitle, null, new RectF(x, y, x + width, y + height), titlePaint);
    }

    Method này tính kích thước của titleView tương ứng với size của emotion, anh em thấy chỉ là một số phép toán tỷ lệ thui, đặt giấy bút ra là hiểu ngay ý mà.

    Hì hí, run nào:

    Trạng thái “BEGIN”

    Ngược đời vỡi, cuối bài rùi mới đến BEGIN. Chúng ta cùng xem lại quá trình chuyển động của các emotion nhé:

    Đồ thị biểu diễn chuyển động của emotion như sau:

    Hình bên trái là đồ thị minh hoạ đường đi của emo icon, và hình bên phải là mô phỏng chi tiết vị trí ứng với từng mốc thời gian của emo icon. Vậy việc chúng ta cần làm là điều khiển cho các emo icon di chuyển theo đồ thị trên.

    Đồ thị này được thể hiện bằng một phương trình có tên là EaseOutBack, có khá nhiều đồ thị hay ho mà mình quên xừ mất link rùi, bao giờ mình tìm lại được mình sẽ update lại cho mọi người nhé.

    Giờ ta tạo một class EaseOutBack:

    public class EaseOutBack {
    
        private final float s = 1.70158f;
        private final long duration;
        private final float begin;
        private final float change;
    
        public EaseOutBack(long duration, float begin, float end) {
            this.duration = duration;
            this.begin = begin;
            this.change = end - begin;
        }
    
        public static EaseOutBack newInstance(long duration, float beginValue, float endValue) {
            return new EaseOutBack(duration, beginValue, endValue);
        }
    
        public float getCoordinateYFromTime(float currentTime) {
            return change * ((currentTime = currentTime / duration - 1) * currentTime * ((s + 1) * currentTime + s) + 1) + begin;
        }
    }

    Ta thêm một đối tượng EaseOutBack vào trong class ReactionView để nó thực hiện tính toán Y cho các emotion:

    public class ReactionView extends View {
    
        ...
    
        private EaseOutBack easeOutBack;
    
        ...
    
    }

    Giờ thì ta xóa đoạn code tạm để xác định tọa độ ban đầu các thành phần đi nhé:

    XÓA ở method init:

    //BEGIN: Đoạn này để đặt các thành phần vào vị trí ban đầu để xem kết quả thui,
    //chứ các thành phần ban đầu sẽ bị ẩn đi, vì chưa click like mà :D
    for (int i = 0; i < emotions.length; i++) {
        emotions[i].currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
        emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
    }
    //END

    Mục đích bây giờ là khi ta ấn nút like, các thành phần di chuyển lên, nên suy ra đầu tiên ta phải gán tọa độ cho các thành phần ở vị trí không nhìn thấy bằng cách là cho nó ở ngoài khoảng cách của view cha:

    Method initElement:

    private void initElement() {
        board.currentY = CommonDimen.HEIGHT_VIEW_REACTION + 10;
        for (Emotion e : emotions) {
            e.currentY = board.currentY + CommonDimen.DIVIDE;
        }
    }

    Giờ bắt đầu thực hiện show view nào, hì hí. Cần tí Animation mới nữa nhỉ, ta đã khai báo Animation BeginningAnimation để thực hiện điều này:

    class BeginningAnimation extends Animation {
    
        public BeginningAnimation() {
            beforeAnimateBeginning();
            setDuration(DURATION_BEGINNING_ANIMATION);
        }
    
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            calculateInSessionBeginning(interpolatedTime);
        }
    }

    Method show:

    public void show() {
        state = StateDraw.BEGIN;
        setVisibility(VISIBLE);
        beforeAnimateBeginning();
        startAnimation(new BeginningAnimation());
    }

    Method beforeAnimateBeginning:

    private void beforeAnimateBeginning() {
        board.beginHeight = Board.BOARD_HEIGHT_NORMAL;
        board.endHeight = Board.BOARD_HEIGHT_NORMAL;
    
        board.beginY = Board.BOARD_BOTTOM + 150;
        board.endY = Board.BOARD_Y;
    
        easeOutBack = EaseOutBack.newInstance(DURATION_BEGINNING_EACH_ITEM, Math.abs(board.beginY - board.endY), 0);
    
        for (int i = 0; i < emotions.length; i++) {
            emotions[i].endY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
            emotions[i].beginY = Board.BOARD_BOTTOM + 150;
            emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
        }
    }

    Method calculateInSessionBeginning:

    private void calculateInSessionBeginning(float interpolatedTime) {
        float currentTime = interpolatedTime * DURATION_BEGINNING_ANIMATION;
    
        if (currentTime > 0) {
            board.currentY = board.endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 100) {
            emotions[0].currentY = emotions[0].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 100, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 200) {
            emotions[1].currentY = emotions[1].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 200, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 300) {
            emotions[2].currentY = emotions[2].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 300, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 400) {
            emotions[3].currentY = emotions[3].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 400, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 500) {
            emotions[4].currentY = emotions[4].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 500, DURATION_BEGINNING_EACH_ITEM));
        }
    
        if (currentTime >= 600) {
            emotions[5].currentY = emotions[5].endY + easeOutBack.getCoordinateYFromTime(Math.min(currentTime - 600, DURATION_BEGINNING_EACH_ITEM));
        }
    
        invalidate();
    }

    Ở đây anh em sẽ thấy DURATION_BEGINNING_ANIMATION = 900 của Animation BeginningAnimation có tí lạ. Mình sẽ giải thích thế này, view của ta có 7 thành phần là 1 board + 6 emotion. Mình muốn các thành phần lần lượt thực hiện di chuyển chứ không muốn cả lũ xuất phát cùng lúc nên mình đặt thế này, board xuất phát đầu tiên, emotion 1 xuất phát lúc 100, emotion 2 xuất phát lúc 200, …và thằng cuối cùng xuất phát lúc 600. Các ông thần này đều thực hiện quãng đường của mình trong 0.3s => Tổng thời gian 7 ông thần kia thực hiện mất 600 (0.6s) + 0.3s cho ông cuối thực hiện nốt là 0.9s như ta thấy.

    Quay lại với thím MainActivity nào:

    public class MainActivity extends AppCompatActivity {
    
        Button btnLike;
        ReactionView reactionView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initView();
        }
    
        private void initView() {
            btnLike = (Button) findViewById(R.id.btn_like);
            reactionView = (ReactionView) findViewById(R.id.view_reaction);
            reactionView.setVisibility(View.INVISIBLE);
            btnLike.setOnClickListener(view -> reactionView.show());
        }
    }

    Hết hơi!!! Run thui rùi té đi ngủ nào, giờ là 2h30AM đó ~~

    Xin giới thiệu với anh em đồng chí Thành Văn Quả:

    Bài hơi dài nhỉ, vất vả cho anh em rùi :yum: .Link full source code

    Hết bài rùi, sắp tới nếu có thời gian mình sẽ build một thư viện cho thằng này để mọi người dễ sử dụng và customize hơn :blush:.

    Chúc anh em cuối tuần vui vẻ :kissing_closed_eyes::kissing_closed_eyes::kissing_closed_eyes:

  • Flutter !!!

    Flutter !!!

    Flutter là gì ?

    Flutter là một mobile UI technology & SDK from Google, nó cho phép bạn build native apps đa nền tảng (Android & iOS) với hiệu suất và độ trung thực cao trong 1 thời gian ngắn, tiết kiệm chi phí.

    Flutter hoạt động với những code có sẵn được sử dụng bởi các lập trình viên và tổ chức trên thế giới.

    Tại sao lại là Flutter ?

    Ưu điểm:

    • Dễ dàng cài đặt và sử dụng (cái này thì ăn đứt React-Native , sure win), bộ doc khá ngon và đầy đủ. Chỉ cần tải flutter từ git về, chạy command line “flutter doctor” nó sẽ báo cho bạn tất cả những vấn đề đang tồn tại trong hệ thống của bạn.
    • Phát triển cho cả iOS và Android trên cùng 1 codebase nên sẽ tiết kiệm được chi phí và thời gian.
    • Tuy là 1 cross-platform nhưng hiệu năng và giao diện đạt tới mức gần như native app.
    • Flutter hoàn toàn free và là open-source.
    • Ngôn ngữ Dart: Dart — OOP, những dev quen làm việc với Java và C# sẽ bắt nhịp rất nhanh với Dart.
    • Animation và trải nghiệm cài đặt của Flutter thực sự tốt và mượt mà như 1 native app.

    Nhược điểm:

    • Animation: Rất khó để tạo ra 1 animation riêng biệt của mình.
    • Giới hạn về thư viện.
    • Framework còn khá “trẻ”, cần thêm thời gian để phát triển.

    Đặc điểm nổi bật của Flutter

    • Fast Development
    • Hot reload: Mỗi khi update source, thay vì phải build lại app như native, thì chúng ta chỉ cần 1 phím “r” thần thánh (nếu dùng command line để build) là app sẽ được update ngay lập tức trong vòng 1–2s.
    • Flutter có cầu nối là Dart, kích thước ứng dụng lớn hơn, nhưng nó hoạt động nhanh hơn nhiều. Không giống như React Native với cầu nối là Javascript.
    • Hỗ trợ tốt cho các IDE (Android Studio ,IntelliJ ,VS Code).
    • Trình điều hướng được tích hợp sẵn.
    • Expressive & Flexible UI: Flutter có thể làm thoả mãn những người dùng khó tính nhất với các widget built-in đẹp mắt theo Material Design và Cupertino (iOS — flavor), scroll mượt mà, dễ dàng thao tác với các UI component.
    • Truy cập các tính năng và SDK native: Flutter cho phép bạn sử dụng lại code Java, Swift, Objective-C hiện tại của mình và truy cập các tính năng và SDK native trên iOS và Android.
    • Native performance.
    • Flutter có các công cụ và thư viện để giúp bạn dễ dàng đưa ý tưởng của mình vào cuộc sống trên iOS và Android. Nếu bạn chưa có kinh nghiệm phát triển trên thiết bị di động, thì Flutter là một cách dễ dàng và nhanh chóng để xây dựng các ứng dụng di động tuyệt đẹp. Nếu bạn là một nhà phát triển iOS hoặc Android có kinh nghiệm, bạn có thể sử dụng Flutter cho các View của bạn và tận dụng nhiều code Java / Kotlin / ObjC / Swift hiện có của bạn.

    Link tham khảo: https://flutter.io/

    LET’S TRY ITTTTT !!!!