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:
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
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
}
Đô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:
Đơ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.
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.
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)
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.)
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:
Trạng thái “BEGIN” – là trạng thái các thành phần lúc bắt xuất hiện.
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.
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.
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:
Độ 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 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:
Xử lý độ cao của reaction box giảm dần.
Xử lý kích thước + tọa độ của các emotion.
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.beginHeight và board.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.
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 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ẹ:
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:
Ở đâ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.
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.