Category: Android

  • Custom Lint Rules

    Custom Lint Rules

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

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

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

    Creating your rules module

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

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

    Creating Detector

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

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

    Creating Issue

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

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

    Creating Registry

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

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

    Run Lint

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

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

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

    ./gradlew app:lintDebug

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

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

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

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

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

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

    Link tham khảo:

    • https://github.com/fabiocarballo/lint-sample
  • Detekt

    Detekt

    Xin chào các bạn, lại là tôi đây, bài viết lần này tôi sẽ chỉ cho các bạn cách cải thiện mã nguồn của bạn với Detekt.

    Detekt là gì?
    Detekt là một công cụ để phân tích code cho lập trình kotlin. Nó hoạt động dựa trên các cú phúp trừu tượng được cung cấp bởi trình biên dịch kotlin.

    Detekt có những đặc trưng nào?

    • Phân tích code smell cho các dự án kotlin của bạn.
    • Báo cáo độ phức tạp dựa trên các dòng code. Độ phức tạp của McCabe và số lượng code smells.
    • Cấu hình cao (rule set or rule level)
    • Loại bỏ các phát hiện với chú thích của kotlin là @Suppress và java là @SuppressWarnings
    • Xác định ngưỡng code smell sẽ phá vỡ bản build của bạn và in ra cảnh báo.
    • Code smell baseline và bỏ qua danh sách legacy của dự án.
    • Gradle Plugin để phân tích code qua Gradle Build.
    • Grade task sử dụng IntelliJ để định dạng và kiểm tra mã kotlin.
    • Tuỳ chọn cấu hình của detekt cho mỗi module bằng cách sử dụng profiles (gradle-plugin).
    • Tích hợp SonarQube.
    • Có thể mở rộng bằng quy tắc riêng và FileProcessListener’s.
    • Tích hợp IntelliJ.

    Sử dụng detekt như nào?
    Trước tiên, bạn hãy cấu hình trong gradle build file.

    configurations {
        detekt
    }
    
    task detekt(type: JavaExec) {
        main = "io.gitlab.arturbosch.detekt.cli.Main"
        classpath = configurations.detekt
        def input = "$projectDir/src/"
        def config = "$rootDir/detekt/detekt-config.yml"
        def filters = ".*/techover.detekt/.*"
        def output = "$rootDir/detekt/reports"
        def params = ['-i', input, '-c', config, '-f', filters, '-o', output]
        args(params)
    }
    
    dependencies {
        detekt "io.gitlab.arturbosch.detekt:detekt-cli:$detekt_version"
    }

    Chúng ta cùng đi tìm hiểu trong task detekt bên trên có những gì?
    def input = "$projectDir/src/" là phần nào trong dự án mà bạn muốn được phân tích.
    def config = "$rootDir/detekt/detekt-config.yml" là phần bạn cài đặt sẽ sử dụng những quy tắc nào để phân tích.
    def filters = ".*/techover.detekt/.*" là phần loại bỏ không cần phân tích, với nhiều đường dẫn khác nhau thì bạn sử dụng dấu phẩy để phân tách.
    def output = "$rootDir/detekt/reports" là phần mà khi chạy phân tích sẽ đưa ra tài liệu báo cáo.

    Bạn có thể sử dụng detekt-config.yml tham khảo ở đây.

    Vậy là bạn đã có thể chạy detekt cho dự án của bạn rồi, bằng cách chạy câu lệnh:

    ./gradlew detekt

    Sau khi chạy câu lệnh gradlew bên trên thì sẽ xuất hiện output như dưới đây:

    Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details
    
    > Task :app:detekt
    
    Successfully generated XmlOutputReport.
    Successfully generated PlainOutputReport.
    
    detekt run within 1019 ms
    
    BUILD SUCCESSFUL in 14s
    1 actionable task: 1 executed

    Như vậy là source code của dự án bạn đang không có lỗi nào.
    Nếu như bạn chạy mà source code có lỗi thì sẽ hiển thị như sau:

    > Task :app:detekt
    Ruleset: code-smell
    Ruleset: comments
    Ruleset: complexity
    Ruleset: empty-blocks
    Ruleset: exceptions
    Ruleset: performance
    Ruleset: potential-bugs
    Ruleset: style
            WildcardImport - [ExampleInstrumentedTest.kt] at androidTest/java/techover/detekt/ExampleInstrumentedTest.kt:9:1
            WildcardImport - [ExampleUnitTest.kt] at test/java/techover/detekt/ExampleUnitTest.kt:5:1
            FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
            FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
            MagicNumber - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:15:22
    
    Successfully generated XmlOutputReport.
    Successfully generated PlainOutputReport.
    
    detekt run within 1203 ms
    
    BUILD SUCCESSFUL in 3s
    1 actionable task: 1 executed

    Để bạn có thể hiểu hơn về tập tin cấu hình mà bạn tham khảo ở link bên trên detekt-config.yml thì dưới đây tôi sẽ nói qua về phần này cho bạn hiểu hơn.

    Tập tin cấu hình detekt có những gì?
    Detekt sử dụng tệp cấu hình kiểu yaml cho nhiều thứ khác nhau:

    • Bộ quy tắc và thuộc tính quy tắc.
    • Build thất bại.
    • Bộ xử lý tệp kotlin.
    • Console và định dạng đầu ra.

    Bộ quy tắc và quy tắc:
    Detekt cho phép dễ dàng chỉ cần chọn các quy tắc bạn muốn và cấu hình chúng theo cách bạn muốn. Ví dụ, nếu bạn muốn cho phép tối đa 20 chức năng trong tệp Kotlin thay vì ngưỡng mặc định là 10.

    complexity:
      TooManyFunctions:
        threshold: 20

    Bộ lọc đường dẫn / Không bao gồm / Bao gồm:
    Bắt đầu với phiên bản RC15 bộ lọc đường dẫn có thể được xác định cho từng quy tắc hoặc bộ quy tắc:

    complexity:
      TooManyFunctions:
        ...
        excludes: "**/internal/**"
        includes: "**/internal/util/NeedsToBeChecked.kt"

    Bảng điều khiển và báo cáo đầu ra:

    console-reports:
      active: true
      exclude:
      #  - 'ProjectStatisticsReport'
      #  - 'ComplexityReport'
      #  - 'NotificationReport'
      #  - 'FindingsReport'
      #  - 'FileBasedFindingsReport'
      #  - 'BuildFailureReport'
    
    output-reports:
      active: true
      exclude:
      #  - 'HtmlOutputReport'
      #  - 'TxtOutputReport'
      #  - 'XmlOutputReport'

    Bộ vi xử lý (Bộ xử lý thường được sử dụng để nâng cao số liệu dự án):

    processors:
      active: true
      exclude:
      # - 'FunctionCountProcessor'
      # - 'PropertyCountProcessor'
      # - 'ClassCountProcessor'
      # - 'PackageCountProcessor'
      # - 'KtFileCountProcessor'

    Link tham khảo:

    • https://arturbosch.github.io/detekt/index.html
    • https://github.com/arturbosch/detekt

  • Tổng quan về Mobile App

    Tổng quan về Mobile App

    Ghi chú:  Bài viết này chỉ là một góc nhìn chủ quan của tác giả về mảng mobile app. vì vậy có gì không đúng mọi người có thể đóng góp ở phần comment nhé!! Thank.

    Mở đầu:
    Ở thời điểm hiện tại việc xây dựng ứng dụng native không phải là lựa chọn duy nhất để tạo lên một một ứng dụng mobile app. Ngày nay chúng ta có thể dựa vào yêu cầu của khách hàng, các chức năng của sản phẩm để lựa chọn được hướng đi phù hợp hơn. Ta có thể dựa trên vào công nghệ web (HTML5, CSS3 và JavaScript) đang phát triển mạnh mẽ trên mobile. Hoặc tận hưởng những lợi ích của các công cụ phát triển đa nền tảng như React Native hoặc Flutter. Dưới đây, bạn sẽ tìm thấy chìa khóa để giải quyết vấn đề khó khăn này khi chọn phương pháp phát triển ứng dụng di động.

    Native App

    Native app hay còn được gọi là ứng dụng gốc. Vốn dĩ nó có cái tên này là bởi vì nó được viết bằng chính các ngôn ngữ lập trình gốc thần nhất dành riêng cho từng nền tảng cụ thể. Hai nền tảng di động phổ biến nhất hiện nay là Android và iOS (Windows Phone thì đã bị khai tử vào tháng 10/ 2017 ). Từ đó, các ngôn ngữ lập trình tương ứng được chính các công ty mẹ tạo ra phù hợp với từng nền tảng. Chẳng hạn như Apple đã có Swift, Objecive-C được dành cho lập trình ứng dụng trên nền tảng iOS. Lập trình trên Android thì dùng Java, mặc dù đây không phải ngôn ngữ do Google tạo ra.

    Viết Native App nghĩa là lập trình viên sẽ sử dụng IDE, SDK mà nhà sản xuất cung cấp để lập trình ra một ứng dụng, build ứng dụng đó thành file cài và gửi lên App Store để kiểm duyệt. Người dùng sẽ phải tìm ứng dụng trên App Store, tải về máy và chạy.  

    Với những hệ thống lớn, cần đồng bộ, ta vẫn phải viết phần back-end trên server. Server sẽ đưa ra một số API. Native app lấy dữ liệu về máy, truyền dữ liệu lên server thông qua các API này.

    Ưu điểm

    • Tận dụng được toàn bộ những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
    • Có thể chạy được offline.
    • Performance rất nhanh vì code native sẽ được chạy trực tiếp.
    • UX phù hợp với từng nền tảng
    • Là lựa chọn duy nhất cho các ứng dụng game, xử lý hình ảnh hay video …

    Khuyết điểm

    • Cần cài đặt nặng nề (Android Studio, XCode, Android SDK, …), khó tiếp cận.
    • Với mỗi hệ điều hành, ta phải viết một ứng dụng riêng. Khó đảm bảo sự đồng bộ giữa các ứng dụng (1 button trên Android sẽ khác 1 button trên iOS, pop cũng khác).
    • Cần phải submit app lên App Store, mỗi lần update phải thông báo người dùng.
    • Code mệt và lâu hơn so với Mobile Web dẫn đến một khuyết điểm là chi phí phát triển cao.

    Kĩ năng cần có

    • Ngôn ngữ lập trình: Java / Kotlin cho Android, Objective-C / Swift cho iOS
    • Kiến thức chuyên sâu về ứng dụng: View, Action, Adapter trong Android …
    • Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, …

    __________________________________________________________________________

    Hybrid App

    Hybrid App kết hợp những ưu điểm của Mobile Web và Native App. Ta xây dựng một ứng dụng bằng HTML, CSS, Javascript, chạy trên WebView của mobile. Tuy nhiên, Hybrid App vẫn có thể tận dụng những tính năng của device: chụp hình, GPS, rung, ….

    Hybrid App sẽ được viết dựa trên một cross-platform framework: Cordova, Phonegap, Ionic …. Ta sẽ gọi những chức năng của mobile thông qua API mà framework này cung cấp, dưới dạng Javascript. Bạn chỉ cần viết một lần, những framework này sẽ tự động dịch ứng dụng này ra các file cài đặt cho Android, iOS . Một số ứng dụng không quá nặng về xử lý, cần tận dụng chức năng của device sẽ chọn hướng phát triển này.

    Ưu điểm

    • Chỉ cần biết HTML, CSS, JS .
    • Viết một lần, chạy được trên nhiều hệ điều hành
    • Tận dụng được các chức năng của device.

    Khuyết điểm

    • Không ổn định, khó debug. Framework sẽ dịch code của bạn thành code native, việc sửa lỗi ứng dụng khá khó vì bạn không biết code sẽ được dịch ra như thế nào.
    • Performance chậm.
    • Cần cài đặt nhiều thứ (phải cài đặt SDK này nọ thì mới build ứng dụng được).

    Kiến thức cần biết

    • HTML, CSS, Javscript cơ bản.
    • Cách dùng một số framework CSS, Javascript: jQuery Mobile, Ionic Framework, AngularJS, Bootstrap, …
    • Kiến thức về các cross-platform framework: Cordova, Phonegap
    • Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, … (Hybrid app cũng sẽ kết nối với server thông qua API như Native App).

    __________________________________________________________________________

    Cross-Platform App

    Được sinh ra nhằm mục đích để giải quyết bài toán hiệu năng của Hybrid và bài toán chi phí khi mà phải viết nhiều loại ngôn ngữ native cho từng nền tảng di động. Nhưng chúng ta lại hay nhầm lẫn giữa Hybrid AppCross-Platform App, trên thực tế thì chúng khác hoàn toàn nhau. Có lẽ, đặc điểm chung duy nhất giữa chúng là khả năng chia sẻ source code. Lập trình viên chỉ cần lập trình một lần và biên dịch hoặc phiên dịch ra thành nhiều bản Native App tương ứng với từng nền tảng khác nhau.

    Công cụ quan trọng nhất để thực hiện các dự án ứng dụng đa nền tảng (Cross Platform) chính là Frameworks đa nền tảng. Có rất nhiều Framework đa nền tảng. Mỗi loại sẽ có những điểm mạnh và điểm yếu khác nhau. Tùy vào mục tiêu xây dựng App mà lập trình viên sẽ lựa chọn Framework nào cho phù hợp.

    Nổi tiếng và phổ biến nhất là Framework Xamarin. Ngôn ngữ lập trình chủ đạo trong Xamarin là C#, ngoài ra còn có Objective-C, Swift và Java. Ngoài ra, còn một số cái tên mà khá hot đó là React-Native (thằng này có ông bô là Facebook ), Flutter (thằng này có ông bác là Google)…

    Ưu điểm

    • Tận dụng được những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
    • Hiệu năng tương đối ổn định.
    • Tiết kiệm tiền.
    • Hiệu quả về mặt thời gian khi mà bạn muốn phát triển một ứng dụng nhanh chóng.
    • Trải nghiệm người dùng tốt hơn là hybrid app.

    Nhược điểm

    • Hiệu năng sẽ thấp hơn với app native code.
    • Khó học vẫn đòi hỏi kiến thức native code.
    • Vẫn còn có hạn chế từ framework

    Kĩ năng cần có

    • Kiến thức C# (đối với Xamarin ), JS (đối với React-Native ), Dart(đối với Flutter) Objective-C, Swift và Java cơ bản.
    • Kiến thức về một số framework React-Native, Xamarin …

    __________________________________________________________________________

    Web App

    Hướng Mobile Web thường được áp dụng khi các bạn đã có sẵn một website đang hoạt động. Ta sẽ tạo thêm 1 trang web riêng cho mobile, sử dụng HTML, CSS, một số framework hỗ trợ mobile và responsive (Bootstrap, jQuery Mobile, Materialize). Người dùng sẽ trang web dành cho mobile để dùng ứng dụng.

    Các xử lý khác liên quan đến backend như database sẽ được thực hiện phía trên server. Với một số framework như Angular, VueJS … một trang web có thể giống y hệt một ứng dụng di động thật sự.

    Ưu điểm

    • Chỉ cần có kiến thức về web là viết được
    • Viết một lần, chạy được trên mọi hệ điều hành
    • Người dùng không cần phải cài app, có thể vào thẳng trang web
    • Không cần phải thông qua App Store, tiết kiệm tiền
    • Dễ nâng cấp (Chỉ việc nâng cấp web là xong)

    Nhược điểm

    • Với một số máy đời cũ, Web App sẽ bị bể giao diện, hiển thị sai, hoặc javascript không chạy.
    • Performance chậm
    • Không thể tận dụng được các tính năng của di động: Push notification, chụp hình, nghiêng máy, định vị GPS…

    Kĩ năng cần có

    • Kiến thức HTML, CSS, Javascript cơ bản.
    • Kiến thức về một số framework responsive/mobile như: jQuery Mobile, Bootstrap, …
    • Một số framework javascript để viết Single Page Application: AngularJS, VueJS, …

    Kết Bài

    Sorry các bạn bài viết hơi dài, sau khi nhìn tổng quan về mobile app thì các bạn đã chọn cho mình hướng đi nào chưa? còn mình thì sẽ tiếp tục theo hướng Cross-Platform app.
    Cảm ơn các bạn đã đọc đến đây nhé.

    Tham khảo: https://railsware.com/blog/native-vs-hybrid-vs-cross-platform/

  • Media Player (Part2) – Add Record function vào IJKPlayer trên Android

    Media Player (Part2) – Add Record function vào IJKPlayer trên Android

    Điểm mạnh của IJKPlayer là low latency, nó có độ trễ khá thấp khi streaming, nhưng giả sử phát sinh tình huống cần record một đoạn video khi đang streaming thì phải làm thế nào? IJKPlayer không support sẵn.

    Sau khi tham khảo 1 số blog của các bạn…..Trung Quốc và build được thành công thì mình share lại thông tin cho ai cần(vâng, ko hiểu tại sao cứ liên quan đến video, camera,Streaming thì tài liệu chỉ có thể tìm thấy bên….Trung Quốc. Không phủ nhận, các bạn ý giỏi thật  )

    Trước hết, các bạn cần build được IJKPlayer cho Android đã nhé, cụ thể có thể xem link bên dưới

    OK, Sau khi các bạn biết cách chuẩn bị môi trường và build thành công, chúng ta cần tiến hành chỉnh sửa một chút trong thư viện IJKplayer(Mình sẽ không đề cập đến vấn đề pháp lý, licence ở đây)

    Chúng ta cần check out source code và build thử nhé

    git clone https://github.com/baka3k/IjkPlayerRecorder.git
    cd IjkPlayerRecorder
    // Khởi tạo project build cho Android
    ./init-android.sh
    //IJKPlayer có sử dụng ffmpeg - nên việc build ffmpeg là bắt buộc
    cd android/contrib
    ./compile-ffmpeg.sh clean
    ./compile-ffmpeg.sh all
    
    //quay lại thư mục IJK và build IJKPlayer
    cd ..
    ./compile-ijk.sh all

    Nếu thành công – tức là bạn đã setup đầy đủ, có thể build IJKPlayer được, sẵn sàng cho việc thêm code để thêm chức năng recorder cho IJKPlayer.

    Tiếp tục nhé

    1.Trong thư mục ijkmedia/ijkplayer bạn tìm đến file ff_ffplay.h và khai báo các hàm sau:

    Refer:

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay.h
    /* record rtsp streaming */
    int ffp_start_recording_l(FFPlayer *ffp, const char *file_name);
    int ffp_record_isfinished_l(FFPlayer *ffp);
    int ffp_stop_recording_l(FFPlayer *ffp);
    void ffp_get_current_frame_l(FFPlayer *ffp, uint8_t *frame_buf);

    2.Trong file ff_ffplay.c chúng ta định nghĩa nội dung thân hàm cho 4 function này

    Refer:

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay.c
    int ffp_start_recording_l(FFPlayer *ffp, const char *file_name)
    {
    assert(ffp);
    VideoState *is = ffp->is;
    
    ffp->m_ofmt_ctx = NULL;
    ffp->m_ofmt = NULL;
    ffp->is_record = 0;
    ffp->record_error = 0;
    
    if (!file_name || !strlen(file_name)) {
    av_log(ffp, AV_LOG_ERROR, "filename is invalid");
    goto end;
    }
    
    if (!is || !is->ic || is->paused || is->abort_request) {
    av_log(ffp, AV_LOG_ERROR, "is,is->ic,is->paused is invalid");
    goto end;
    }
    
    if (ffp->is_record) {
    av_log(ffp, AV_LOG_ERROR, "recording has started");
    goto end;
    }
    
    avformat_alloc_output_context2(&ffp->m_ofmt_ctx, NULL, NULL, file_name);
    if (!ffp->m_ofmt_ctx) {
    av_log(ffp, AV_LOG_ERROR, "Could not create output context filename is %s\n", file_name);
    goto end;
    }
    ffp->m_ofmt = ffp->m_ofmt_ctx->oformat;
    
    for (int i = 0; i < is->ic->nb_streams; i++) {
    AVStream *in_stream = is->ic->streams[i];
    AVStream *out_stream = avformat_new_stream(ffp->m_ofmt_ctx, in_stream->codec->codec);
    if (!out_stream) {
    av_log(ffp, AV_LOG_ERROR, "Failed allocating output stream\n");
    goto end;
    }
    
    av_log(ffp, AV_LOG_DEBUG, "in_stream->codec;%p\n", in_stream->codec);
    if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Failed to copy context from input to output stream codec context\n");
    goto end;
    }
    
    out_stream->codec->codec_tag = 0;
    if (ffp->m_ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
    out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
    }
    
    av_dump_format(ffp->m_ofmt_ctx, 0, file_name, 1);
    
    if (!(ffp->m_ofmt->flags & AVFMT_NOFILE)) {
    if (avio_open(&ffp->m_ofmt_ctx->pb, file_name, AVIO_FLAG_WRITE) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Could not open output file '%s'", file_name);
    goto end;
    }
    }
    
    if (avformat_write_header(ffp->m_ofmt_ctx, NULL) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Error occurred when opening output file\n");
    goto end;
    }
    
    ffp->is_record = 1;
    ffp->record_error = 0;
    pthread_mutex_init(&ffp->record_mutex, NULL);
    
    return 0;
    end:
    ffp->record_error = 1;
    return -1;
    }
    
    int ffp_record_isfinished_l(FFPlayer *ffp)
    {
    return 0;
    }
    
    int ffp_record_file(FFPlayer *ffp, AVPacket *packet)
    {
    assert(ffp);
    VideoState *is = ffp->is;
    int ret = 0;
    AVStream *in_stream;
    AVStream *out_stream;
    
    if (ffp->is_record) {
    if (packet == NULL) {
    ffp->record_error = 1;
    av_log(ffp, AV_LOG_ERROR, "packet == NULL");
    return -1;
    }
    
    AVPacket *pkt = (AVPacket *)av_malloc(sizeof(AVPacket));
    av_new_packet(pkt, 0);
    if (0 == av_packet_ref(pkt, packet)) {
    pthread_mutex_lock(&ffp->record_mutex);
    if (!ffp->is_first) {
    ffp->is_first = 1;
    pkt->pts = 0;
    pkt->dts = 0;
    } else {
    if (pkt->stream_index == AVMEDIA_TYPE_AUDIO) {
    pkt->pts = llabs(pkt->pts - ffp->start_a_pts);
    pkt->dts = llabs(pkt->dts - ffp->start_a_dts);
    }
    else if (pkt->stream_index == AVMEDIA_TYPE_VIDEO) {
    pkt->pts = pkt->dts = llabs(pkt->dts - ffp->start_v_dts);
    }
    }
    
    in_stream = is->ic->streams[pkt->stream_index];
    out_stream = ffp->m_ofmt_ctx->streams[pkt->stream_index];
    
    pkt->pts = av_rescale_q_rnd(pkt->pts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
    pkt->dts = av_rescale_q_rnd(pkt->dts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
    pkt->duration = av_rescale_q(pkt->duration, in_stream->time_base, out_stream->time_base);
    pkt->pos = -1;
    
    if ((ret = av_interleaved_write_frame(ffp->m_ofmt_ctx, pkt)) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Error muxing packet\n");
    }
    
    av_packet_unref(pkt);
    pthread_mutex_unlock(&ffp->record_mutex);
    } else {
    av_log(ffp, AV_LOG_ERROR, "av_packet_ref == NULL");
    }
    }
    return ret;
    }
    
    int ffp_stop_recording_l(FFPlayer *ffp){
    assert(ffp);
    if (ffp->is_record) {
    ffp->is_record = 0;
    pthread_mutex_lock(&ffp->record_mutex);
    if (ffp->m_ofmt_ctx != NULL) {
    av_write_trailer(ffp->m_ofmt_ctx);
    if (ffp->m_ofmt_ctx && !(ffp->m_ofmt->flags & AVFMT_NOFILE)) {
    avio_close(ffp->m_ofmt_ctx->pb);
    }
    avformat_free_context(ffp->m_ofmt_ctx);
    ffp->m_ofmt_ctx = NULL;
    ffp->is_first = 0;
    }
    pthread_mutex_unlock(&ffp->record_mutex);
    pthread_mutex_destroy(&ffp->record_mutex);
    av_log(ffp, AV_LOG_DEBUG, "stopRecord ok\n");
    } else {
    av_log(ffp, AV_LOG_ERROR, "don't need stopRecord\n");
    }
    return 0;
    }
    void ffp_get_current_frame_l(FFPlayer *ffp, uint8_t *frame_buf)
    {
    ALOGD("=============>start snapshot\n");
    
    VideoState *is = ffp->is;
    Frame *vp;
    int i = 0, linesize = 0, pixels = 0;
    uint8_t *src;
    
    vp = &is->pictq.queue[is->pictq.rindex];
    int height = vp->bmp->h;
    int width = vp->bmp->w;
    
    ALOGD("=============>%d X %d === %d\n", width, height, vp->bmp->pitches[0]);
    
    // copy data to bitmap in java code
    linesize = vp->bmp->pitches[0];
    src = vp->bmp->pixels[0];
    pixels = width * 4;
    for (i = 0; i < height; i++)
    {
    memcpy(frame_buf + i * pixels, src + i * linesize, pixels);
    }
    ALOGD("=============>end snapshot\n");
    }

    3. Define thêm các cấu trúc dữ liệu cần thiết trong ff_ffplay_def.h

    Refer(line 724->733)

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay_def.h
    AVFormatContext *m_ofmt_ctx;
    AVOutputFormat *m_ofmt;
    pthread_mutex_t record_mutex;
    int is_record;
    int record_error;
    int is_first;
    int64_t start_v_pts;
    int64_t start_v_dts;
    int64_t start_a_pts;
    int64_t start_a_dts;

    4. Define thêm hàm record trong ijkplayer.c(đây chính là class player mà chúng ta sẽ phải viết thêm JNI để câu xuống)

    Define hàm vào header file ijkplayer.h

    Refer

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ijkplayer.h
    int ijkmp_start_recording(IjkMediaPlayer *mp, const char *filePath);
    int ijkmp_stop_recording(IjkMediaPlayer *mp);
    int ijkmp_isRecording(IjkMediaPlayer *mp);
    void ijkmp_get_current_frame(IjkMediaPlayer *mp,uint8_t *frame_buf);

    Viết thân hàm vào ijkplayer.c

    Refer

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ijkplayer.c
    static int ijkmp_start_recording_l(IjkMediaPlayer *mp, const char *filePath)
    {
    av_log(mp->ffplayer,AV_LOG_INFO,"cjz ijkmp_start_recording_l filePath %s",filePath);
    return ffp_start_recording_l(mp->ffplayer, filePath);
    }
    
    int ijkmp_start_recording(IjkMediaPlayer *mp,const char *filePath)
    {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_start_recording --- ");
    int retval = ijkmp_start_recording_l(mp,filePath);
    printf("ijkmp_start_recording return == %d\n",retval);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
    }
    
    static int ijkmp_stop_recording_l(IjkMediaPlayer *mp)
    {
    return ffp_stop_recording_l(mp->ffplayer);
    }
    
    int ijkmp_stop_recording(IjkMediaPlayer *mp)
    {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_stop_recording");
    int retval = ijkmp_stop_recording_l(mp);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
    }
    
    static int ijkmp_isRecordFinished_l(IjkMediaPlayer *mp)
    {
    return ffp_record_isfinished_l(mp->ffplayer);
    }
    
    int ijkmp_isRecordFinished(IjkMediaPlayer *mp)
    {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_isRecordFinished ");
    int retval = ijkmp_isRecordFinished_l(mp);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
    }
    
    int ijkmp_isRecording(IjkMediaPlayer *mp) {
    return mp->ffplayer->is_record;
    }
    void ijkmp_get_current_frame(IjkMediaPlayer *mp,uint8_t *frame_buf){
    ffp_get_current_frame_l(mp->ffplayer,frame_buf);
    }

    5. Cuối cùng, update thêm JNI để có thể call được từ tầng java xuống C nhé

    Refer:

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/android/ijkplayer_jni.c

    Các bạn tìm đến đường dẫn IjkPlayerRecorder/ijkmedia/ijkplayer/android/ijkplayer_jni.c

    Và thêm vào

    static jint
    IjkMediaPlayer_startRecord(JNIEnv *env, jobject thiz,jstring file)
    {
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: startRecord: null mp", LABEL_RETURN);
    const char *nativeString = (*env)->GetStringUTFChars(env, file, 0);
    retval = ijkmp_start_recording(mp,nativeString);
    
    LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
    }
    
    static jint
    IjkMediaPlayer_stopRecord(JNIEnv *env, jobject thiz)
    {
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: stopRecord: null mp", LABEL_RETURN);
    
    retval = ijkmp_stop_recording(mp);
    
    LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
    }
    
    static jboolean
    IjkMediaPlayer_getCurrentFrame(JNIEnv *env, jobject thiz, jobject bitmap)
    {
    jboolean retval = JNI_TRUE;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: getCurrentFrame: null mp", LABEL_RETURN);
    
    uint8_t *frame_buffer = NULL;
    
    if (0 > AndroidBitmap_lockPixels(env, bitmap, (void **)&frame_buffer)) {
    (*env)->ThrowNew(env, "java/io/IOException", "Unable to lock pixels.");
    return JNI_FALSE;
    }
    
    ijkmp_get_current_frame(mp, frame_buffer);
    
    if (0 > AndroidBitmap_unlockPixels(env, bitmap)) {
    (*env)->ThrowNew(env, "java/io/IOException", "Unable to unlock pixels.");
    return JNI_FALSE;
    }
    
    LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
    }

    6. Chỉnh sửa các function để JNI map với java

    Trong file ijkplayer_jni.c, tìm đến line 1190 có đoạn define

    static JNINativeMethod g_methods[] = {....

    Add thêm

    { "startRecord", "(Ljava/lang/String;)I", (void *) IjkMediaPlayer_startRecord },
    { "stopRecord", "()I", (void *) IjkMediaPlayer_stopRecord },
    { "getCurrentFrame", "(Landroid/graphics/Bitmap;)Z", (void *) IjkMediaPlayer_getCurrentFrame },

    Refer

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/android/ijkplayer_jni.c

    Việc update code đến đây là OK

    Chúng ta build lại IJKPlayer

    cd android/contrib
    ./compile-ffmpeg.sh clean
    ./compile-ffmpeg.sh all
    
    cd ..
    ./compile-ijk.sh all

    Chờ đợi khá lâu đấy, nếu build lần đầu tiên có khả năng mất 1 tiếng :(, sau khi kết thúc build các bạn sẽ tìm thấy các file .so trong thư mục

    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv5/src/main/libs/armeabi
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a

    Copy các file SO này vào project sample

    IjkPlayerRecorder/android/ijkplayer/ijkplayer-example
    

    và thử thôi

  • Media Player (Part1) – RTSP Player Android

    Media Player (Part1) – RTSP Player Android

    Có nhiều cách để play RTSP trên Android, ứng cử viên hàng đầu là VideoView sẵn có trên Android SDK. Tuy nhiên Video View có rất nhiều hạn chế khi chúng ta cần custom lại, ví dụ chỉnh sửa thêm thắt vào protocol,  add các hiệu ứng hình ảnh vào video khi đang play, record, chuyển đổi các track..etc. Khi đó chúng ta phải lựa chọn một giải pháp khác, cụ thể ở đây mình đang nói đến

    1. Exoplayer
    2. IJKPlayer

    Exoplayer (https://github.com/google/ExoPlayer) open source, apache licence, java core, rất dễ dàng để anh em lập trình android/java tiếp xúc nhưng điểm trừ của nó là độ trễ – low latency

    Khi test với 1 số server RTSP, ExoPlayer cho ra độ trễ vào khoảng 1.2s đến 2.5s tùy chất lượng video out put. Còn IJKPlayer(https://github.com/bilibili/ijkplayer) thì có thể đạt 0.6s đến 0.8s.  Điểm trừ của IJKPlayer là licence, IJKPlayer sử dụng FFMPEG, một thư viện rất nổi tiếng, nên nếu ko muốn dính dáng đến pháp lý khi bán sản phẩm, bạn nên cẩn thận khi lựa chọn nó cho khách hàng hoặc nhúng vào sản phẩm.

    Để build được IJKPlayer cũng khá đơn giản(nếu bạn chuẩn bị đủ môi trường), hướng dẫn này mình viết chạy trên môi trường MAC OS nhé, Window hoặc Ubuntu cách làm tương tự, tuy nhiên môi trường chuẩn bị sẽ khác đi 1 chút

    Check out source code

    Các bạn có thể lấy source IJK từ nhánh chính ở trên hoặc từ repo bên dưới, repo này mình đã add thêm một vài  chỉnh sửa liên quan đến RTSP và Recorder

    Option 1: https://github.com/baka3k/IjkPlayerRecorder(có recorder)

    Option 2: https://github.com/bilibili/ijkplayer(nguyên bản)

    Cài đặt NDK cho Mac: mình test thực tế thì thấy build trên bản NDK 10, NDK16(khá cũ) thì ok nhất, ko cần sửa lỗi và chỉnh lại config build, tất nhiên các bạn pro hơn có thể thử với version NDK khác

    https://dl.google.com/android/repository/android-ndk-r10e-darwin-x86_64.zip

    Giải nén NDK, chỉnh lại enviroment path về thư mục NDK vừa download

    Với window thì System>Advanced System setting>Enviroment variable, giống khi các bạn add path cho adb hoặc sdk

    Với Mac OS thì sửa file  ~/.bash_profile hoặc ~/.zshrc hoặc ~/.profile – tùy thuộc vào bạn đang dùng command build nào nhé 🙂

    # export ANDROID_SDK=<your sdk path> # export ANDROID_NDK=<your ndk path>

    Buid nào:

    Kiểm tra kết nối internet(khi build cần tải 1 số package về, nên tốt nhất là full mạng ko qua proxy gì cả)

    Install home brew, git, yasm (chạy lần lượt các lệnh sau theo step nhé)

    # install homebrew, git, yasm
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    brew install git
    brew install yasm

     Build Android

    git clone https://github.com/baka3k/IjkPlayerRecorder.git
    cd IjkPlayerRecorder
    ./init-android.sh
    
    cd android/contrib
    ./compile-ffmpeg.sh clean
    ./compile-ffmpeg.sh all
    
    cd ..
    ./compile-ijk.sh all

    Sau khi build xong các bạn có thể tìm thấy file .so trong thư mục

    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv5/src/main/libs/armeabi
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a

    Và có thể copy những file .so này vào thư mục sample sẵn có để chạy thử

    Sample đặt tại đường dẫn

    - IjkPlayerRecorder/android/ijkplayer/ijkplayer-example

    Lúc này nó như là 1 app android thông thường, đã có sẵn JNI để call xuống .so nhé 🙁

    Have fun 🙂

  • [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 !!!!