Author: KINH HOANG` HON

  • Các bước hướng dẫn chi tiết submit ứng dụng lên Google Store

    Các bước hướng dẫn chi tiết submit ứng dụng lên Google Store

    Khi bạn đã có trong tay một sản phẩm tốt và đầy tâm huyết. Vậy làm cách nào để có thể giới thiệu ứng dụng của mình đến với nhiều người dùng? Để mọi người biết đến và có thể sử dụng nó? Để nhận được những góp ý giúp cải thiện hơn, hay cũng là một nguồn thu nhập của bạn, thì bạn sẽ phải làm gì?. Chắc chắn, với một developer phát triển về mobile thì bạn sẽ có những câu hỏi như vậy, phải không?

    Để giải đáp cho câu hỏi đó thì iOS bạn sẽ sử dụng App Store để có sumit ứng dụng của bạn, App Store chính là market chính và có nhiều người iOS biết tới nhất.
    Với Android, đó chính là Google Play Store. Google Play Store cũng là market chính và có nhiều người dùng Android biết tới nhất. Vậy làm cách nào để có thể submit ứng dụng lên Google Store? Đừng lo, bài viết này mình sẽ giới thiệu tới các bạn luôn đây. Let’s start…

    Nội dung chính của bài viết:

    • Bạn cần chuẩn bị gì trước khi submit ứng dụng lên Google Store?
    • Cần tối ưu kích thước APK file
    • Từng bước submit ứng dụng lên Google Store
    • Đăng kí tài khoản Google Developer
    • Tạo ứng dụng mới và điền thông tin mô tả ứng dụng
    • Upload logo và screenshot ứng dụng
    • Tiến hành upload APK lên Store
    • Hoàn thành đánh giá Content rating
    • Đăng kí ứng dụng miễn phí hay trả phí

    Bạn cần chuẩn bị gì trước khi submit ứng dụng lên Google Store?

    Khác với Apple, khi mà App Store có quá trình review rất chặt chẽ. Các ứng dụng khi submit lên App Store đều trải qua quá trình review thủ công. Điều này sẽ đảm bảo ứng dụng trên App Store có chất lượng tốt nhất trước khi tới tay người dùng.

    Google Play Store thì quá trình review ứng dụng thường làm bằng máy. Do vậy, ứng dụng của bạn có nhiều cơ hội được approve hơn rất nhiều.

    Tuy nhiên, để ứng dụng thành công với hàng nghìn, thậm chí hàng triệu lượt tải thì bạn cần bỏ công sức ra tối ưu cũng như chuẩn bị kĩ càng trước khi submit.

    Những điều bắt buộc phải làm khi submit ứng dụng lên Google Store

    • Tạo một Bundle ID cho ứng dụng
    • Tạo một APK or Android App Bundles có sign key
    • Và tất nhiên là phải có một tài khoản Google Developer( chi phí để tạo là 25$)

    Cần tối ưu kích thước APK file

    Nếu ứng dụng của bạn có kích thước lớn thì nên chia ra thành nhiều module. Cách làm giống như các Game hay làm vậy. Các bạn chỉ đưa phần chính của ứng dụng lên Store. Sau khi người dùng tải ứng dụng về thì sẽ tiếp tục tải data.

    Tuy nhiên, mình khuyến khích là kích thước APK càng nhỏ càng tốt vì điều đó tốt cho ASO( App Store Optimization). Google Play Store ưu tiên các ứng dụng nhỏ nhẹ, nhưng chất lượng tốt.

    Lưu ý: Google Play supports compressed app downloads of only 150 MB or less.

    Đăng kí tài khoản Google Developer

    Đầu tiên, bạn cần đăng kí trở thành nhà phát triển ứng dụng ở đây: Google Play Console.

    Bạn đăng nhập bằng tài khoản Google như bình thường. Tick vào ô bên dưới Developer Agreement để chuyển sang màn hình thanh toán.

    Sau khi thanh toán bằng thẻ VISA/ MasterCard xong thì bạn cần điền các thông tin cần thiết cho nhà phát triển như: developer name, email address, website, phone number.

    Cuối cùng là nhấn vào nút COMPLETE REGISTRATION.

    Tạo ứng dụng mới và điền thông tin mô tả ứng dụng

    Như vậy là bạn đã trở thành nhà phát triển ứng dụng rồi đấy. Công việc tiếp theo là tạo ứng dụng mới bằng cách nhấn vào nút CREATE APPLICATION

    Lưu ý: Tiêu đề là phần hiển thị trên Google Play chứ không phải tên ứng dụng khi cài vào điện thoại. Nên bạn có thể khéo léo đưa từ khóa vào tiêu đề để tối ưu ASO.

    Tiếp theo, chúng ta cần điền thông tin mô tả ứng dụng: short description và full description.

    Rồi nhấn nút SAVE DRAFT. khi đã điền xong.

    Upload logo và screenshot ứng dụng

    Phần này chúng ta sẽ cần upload ảnh logo, screenshot của ứng dụng. Lưu ý là logo cần có kích thước là 512x512px.

    Nhấn vào Add high-res icon để tải logo lên.

    Chọn Add feature graphic. Đây là ảnh promo được hiển thị trên đỉnh của trang ứng dụng trên Google play.

    Nói chung, bạn có ảnh hay video nào có thể promo được cho ứng dụng thì upload hết lên đây nhé. Càng nhiều càng tốt…

    Cuối cùng là nhấn SAVE DRAFT. để tiếp tục.

    Phía dưới của màn hình có phần chọn kiểu ứng dụng là : App hay Game. Chọn Category phù hợp với ứng dụng: Tool, Productivity, Entertainment…

    Ở màn này có một mục là Content rating. Phần này mình sẽ hướng dẫn chi tiết ở phía dưới bài viết nhé.

    Điền URL tới file privacy policy. Nếu bạn chưa biết cách viết privacy policy như thế nào thì có thể sử dụng công cụ sau để tạo tự động App Privacy Policy Generator

    Bạn sẽ được chuyển đến màn hình như bên dưới.

    Tiến hành upload APK lên Store

    Bạn chọn MANAGE PRODUCTION để tiếp tục hoàn thành các bước tiếp theo.

    Ngoài ra, bạn cũng có thể lựa chọn chạy alpha hay beta testing trước khi thực sự publish ứng dụng cho tất cả người dùng.

    Ở bài viết này, mình không đề cập đển việc chạy alpha hay beta testing. Chúng ta publish chính luôn.

    Nhấn nút CREATE RELEASE.

    Nếu bạn đã tạo APK có sẵn sign key rồi thì không cần phải làm gì cả, chọn luôn opt out.

    Tiếp tục là chọn browse files để upload APK từ máy tính lên.

    Bạn có thể sửa release name, nhưng mình thì cứ để mặc định là số phiên bản của ứng dụng.

    Điền các thông tin chính cho bản apk này (thường thì người ta hay điền các tính năng mới mà cho lần upgrade ứng dụng) rồi nhấn REVIEW.

    APK cũng đã upload lên. Việc tiếp theo như mình nói ở trên là hoàn thành đánh giá Content rating.

    Hoàn thành đánh giá Content rating

    Phần Content rating này, bạn cứ trả lời thật với những câu hỏi của họ là ổn. Các câu hỏi kiểu như: Ứng dụng có liên quan đến SEX không? Ứng dụng có kích động, phản động hay liên quan đến Phát xít không? … Cứ trả lời thật nhé

    Đăng kí ứng dụng miễn phí hay trả phí

    Phần cuối cùng là Pricing & distribution

    Bạn cần cân nhắc là ứng dụng của bạn sẽ phát hành miễn phí hay là bán cho người dùng.

    Mình chỉ lưu ý là: Một khi đã chọn là ứng dụng miễn phí thì bạn không thể chuyển thành ứng dụng trả phí được nữa. Nhưng ngược lại thì được.

    Nếu bạn chọn là ứng dụng trả phí thì cần phải cài đặt phương thức nhận tiền để Google còn thanh toán cho bạn chứ.

    Tất cả đã xong. Việc của bạn bây giờ là quay trở lại App releases và nhấn nút START ROLLOUT TO PRODUCTION và chờ đợi Google review và appove cho ứng dụng của bạn.

    Theo kinh nghiệm của mình thì thời gian review sẽ tầm khoảng 4 giờ. Trong lúc chờ đợi thì hãy đón đọc các bài tiếp theo cũng mình nhé :))

  • Lottie on Android (Part 3)

    Lottie on Android (Part 3)

    Xin chào các bạn, ở bài trước mình có giới thiệu cho các bạn về Animation Listener và Custom Animator với Lottie.
    Trong bài này mình sẽ giới thiệu cho các bạn về cách điều chỉnh các thuộc tính động. Bạn có thể điều chỉnh các thuộc tính động trong thời gian đang chạy của nó. Một số mục đích như:

    • Thay đổi chủ đề.
    • Thay đổi kích thước và thời gian.
    • Đáp ứng với những sự kiện lỗi hay thành công.

    Nội dung bài bao gồm:

    • Hiểu về After Effects để có thể vận dụng vào trong bài này.
    • Cần có những gì để thay đổi?
    • Cách thực hiện nó như thế nào?

    Hiểu về After Effects

    Để điều chỉnh các thuộc tính trong Lottie thì mình cần hiểu các thuộc tính đó.
    Các thuộc tính này được kế thừa từ các thuộc tính trong After Effects. Trong After Effects, nó là tập hợp các Layer ứng với mỗi một thời gian. Đối tượng trong Layer bao gồm: tên, màu sắc, kích thước … Lottie có thể tìm thấy các đối tượng và thuộc tính bằng KeyPath.

    Cần có những gì để thay đổi?

    Để thay đổi thuộc tính trong thời gian chạy, bạn cần có:

    • KeyPath
    • LottieProperty
    • LottieValueCallback

    KeyPath

    KeyPath được sử dụng với một nội dung cụ thể hoặc toàn bộ nội dung cần thay đổi. Nó được xác định bởi một danh sách các chuỗi tương ứng trong cấu trúc phân cấp của After Effects.

    KeyPath bao gồm tên cụ thể của nội dung hoặc ký tự đại diện:

    • Wildcard *: sử dụng để phù hợp với nội dung duy nhất ở vị trí của nó trong KeyPath.
    • Globstar **: sử dụng để phù hợp với không hoặc nhiều layer.

    KeyPath resolution

    KeyPath có khả năng lưu trữ một tham chiếu nội bộ đến nội dung mà họ quyết định. Khi bạn tạo một đối tượng KeyPath mới, nó sẽ không được quyết định. LottieDrawable và LottieAnimationView có một phương thức notifyKeyPath() lấy KeyPath và trả về một danh sách bằng 0 hoặc nhiều quyết định mà mỗi quyết định thành một phần nội dung bên trong. Điều này có thể được sử dụng để khám phá cấu trúc animation của bạn. Để làm như vậy, trong môi trường phát triển, new KeyPath("**") và ghi lại danh sách được trả về. Tuy nhiên, bạn không nên sử dụng ** với ValueCallback vì nó sẽ được áp dụng cho mọi phần nội dung trong animation của bạn. Nếu bạn quyết định KeyPath của mình và muốn thêm một giá trị callback, hãy sử dụng KeyPath được trả về từ phương thức đó vì chúng sẽ được giải quyết nội bộ và sẽ không phải tìm lại nội dung.

    LottieProperty

    LottieProperty là các thuộc tính có thể được set. Chúng tương ứng với giá trị trong After Effects.
    Bạn có thể tham khảo các thuộc tính ở đây.

    ValueCallback

    ValueCallback được gọi mỗi khi animation hoạt động. Nó cung cấp:

    • Khung bắt đầu của khung hình hiện tại.
    • Khung kết thúc của khung hình hiện tại.
    • Giá trị bắt đầu của khung hình hiện tại.
    • Giá trị kết thúc của khung hình hiện tại.
    • Giá trị progress từ 0 tới 1 của khung hình hiện tại với ngoài thời gian interpolation.
    • Giá trị progress của khung hình hiện tại với thời gian interpolator.
    • Progress trong tổng thể animation từ 0 tới 1.

    Ngoài ra, cũng có một số lớp con ValueCallback như LottieStaticValueCallback, nó nhận đúng một giá trị trong constructor và sẽ luôn trả về giá trị đó.

    ValueCallback classes

    • LottieValueCallback: đặt giá trị tĩnh trong contructor or override getValue().
    • LottieRelativeTYPEValueCallback: đặt giá trị trong constructor or override getOffset().
    • LottieInterpolatedTYPEValue: cung cấp giá trị bắt đầu, giá trị kết thúc và tuỳ chọn interpolator để có thời gian.

    Cách thực hiện nó như thế nào?

    Mình vẫn sử dụng trail-loading.jsonbài trước để điều chỉnh.
    Sau đó, bạn hãy xem các thuộc tính và phân cấp của nó:

    Có thể sử dụng log sau:

    loading_animation.resolveKeyPath(KeyPath("**")).forEach {
                Log.i("KeyPath", it.toString())
            }

    Hoặc, có thể sử dụng công cụ Lottie JSON Editor.
    (Với file mình sử dụng bên trên thì sẽ có 5 layer, còn các bạn…). Dưới đây sẽ là một vài ví dụ thay đổi.

    Thay đổi trong một layer

    Mình sẽ thay đổi màu Shape Layer 1("nm": "Shape Layer 1"), trong Ellipse 1("nm": "Ellipse 1") sang màu đỏ như sau:

    loading_animation.addValueCallback(
                KeyPath("Shape Layer 1", "Ellipse 1", "Fill 1"),
                LottieProperty.COLOR, { Color.RED }
            )
    Shape Layer 1

    Hoặc bạn có thể thay đổi màu sắc và kích thước của stroke như sau:

    loading_animation.addValueCallback(
                KeyPath("Shape Layer 1", "Ellipse 1", "Stroke 1"),
                LottieProperty.STROKE_COLOR, { Color.GREEN }
            )
    
            loading_animation.addValueCallback(
                KeyPath("Shape Layer 1", "Ellipse 1", "Stroke 1"),
                LottieProperty.STROKE_WIDTH, { 20f }
            )
    Stroke

    Sử dụng Wildcards

    Mình sử dụng Wildcards để thay đổi toàn bộ layer có Ellipse 1("nm": "Ellipse 1") sang màu GREEN như sau:

    loading_animation.addValueCallback(
                KeyPath("*", "Ellipse 1", "Fill 1"),
                LottieProperty.COLOR, { Color.GREEN }
            )
    Wildcards

    Sử dụng Globstars

    Mình sử dụng Globstars để thay đổi toàn bộ layer kết thúc thuộc tính Fill 1 sang màu BLUE như sau:

    loading_animation.addValueCallback(
                KeyPath("**", "Fill 1"),
                LottieProperty.COLOR, { Color.BLUE }
            )
    Globstars

    Các bạn có thể tham khảo các thuộc tính có thể thay đổi dưới đây:

    Transform:

    • TRANSFORM_ANCHOR_POINT
    • TRANSFORM_POSITION
    • TRANSFORM_OPACITY
    • TRANSFORM_SCALE
    • TRANSFORM_ROTATION

    Fill:

    • COLOR (non-gradient)
    • OPACITY
    • COLOR_FILTER

    Stroke:

    • COLOR (non-gradient)
    • STROKE_WIDTH
    • OPACITY
    • COLOR_FILTER

    Ellipse:

    • POSITION
    • ELLIPSE_SIZE

    Polystar:

    • POLYSTAR_POINTS
    • POLYSTAR_ROTATION
    • POSITION
    • POLYSTAR_OUTER_RADIUS
    • POLYSTAR_OUTER_ROUNDEDNESS
    • POLYSTAR_INNER_RADIUS (star)
    • POLYSTAR_INNER_ROUNDEDNESS (star)

    Repeater:

    • All transform properties
    • REPEATER_COPIES
    • REPEATER_OFFSET
    • TRANSFORM_ROTATION
    • TRANSFORM_START_OPACITY
    • TRANSFORM_END_OPACITY

    Layers:

    • All transform properties
    • TIME_REMAP (composition layers only)

    Bài này đến đây là kết thúc rồi, hãy đón đọc bài viết tiếp theo của mình bạn nhé. Cảm ơn các bạn đã dành thời gian đọc bài của mình. Rất mong nhận được sự góp ý từ các bạn ^^

  • Lottie on Android (Part 2)

    Lottie on Android (Part 2)

    Xin chào các bạn, ở bài trước mình có giới thiệu cho các bạn về Lottie và cách sử dụng Lottie cho Android.
    Trong bài này mình sẽ giới thiệu cho các bạn về Animation Listener và Custom Animator với Lottie.

    Animation Listener

    Có rất nhiều trường hợp khi sử dụng Lottie mà chúng ta cần phải xử lý các công việc khác nữa. Dưới đây là một vài trường hợp cụ thể:

    • Mở một màn hình mới sau khi kết thúc chạy animation với Lottie.
    • Cập nhật giá trị trong khi đang chạy animation với Lottie.
    • Điều chỉnh tốc độ hay thời gian chạy của animation.

    Listener được mô tả như sau:

    loading_animation.addAnimatorUpdateListener { valueAnimator ->
                //do something
            }

    Tham số valueAnimator bên trên thực chất nó là tham số số trong ValueAnimator Class ở Android SDK. Nó cung cấp cho bạn biết về trạng thái hiện tại và thời gian hiện tại của animation.

    Bây giờ, tôi sẽ có 1 bài toán nho nhỏ như sau: ở bài trước tôi sử dụng loading với Lottie, bài này tôi sẽ thực hiện khi loading thì sẽ thực hiện cùng ProgressBar và set giá trị của progress đó hiển thị trên màn hình.

    Tôi sẽ thiết kế layout như sau:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <ProgressBar
            android:id="@+id/progress_horizontal"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:max="100"
            android:progress="0"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintWidth_percent="0.9" />
    
        <TextView
            android:id="@+id/progress_number"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/progress_horizontal"
            app:layout_constraintWidth_percent="0.9"
            tools:text="0/100" />
    
        <com.airbnb.lottie.LottieAnimationView
            android:id="@+id/loading_animation"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    Sau đó, tôi sẽ thực hiện lấy giá trị và set cho ProgressBar như dưới đây:

    package techover.lottie
    
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import kotlinx.android.synthetic.main.activity_main.*
    
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            loading_animation.setAnimation("trail-loading.json")
            loading_animation.loop(true)
            loading_animation.speed = 0.5f
            loading_animation.addAnimatorUpdateListener { valueAnimator ->
                val progress = (valueAnimator.animatedValue as Float * 100).toInt()
                progress_horizontal.progress = progress
                progress_number.text = "$progress / 100"
            }
            loading_animation.playAnimation()
    
        }
    }
    Animation Listener

    Custom Animator

    Để kết hợp Lottie vào ứng dụng để có những hình ảnh mượt mà, sinh động thì thật đơn giản phải không nào? 😉
    Nhưng sẽ có nhiều trường hợp việc kết hợp cũng trở nên quan ngại phần nào: ví dụ như kết hợp Lottie khi download, khi scroll position hay cử chỉ… Vì vậy chúng ta phải Custom Animator để cho phù hợp với từng bài toán.
    Ví dụ sau sẽ giúp các bạn hình dung dễ hơn:

    //Custom animation speed or duration.
            val animator = ValueAnimator.ofFloat(0f, 2f)
            animator.addUpdateListener { valueAnimator: ValueAnimator ->
                loading_animation.speed = valueAnimator.animatedValue as Float
            }
            animator.start()

    Ở đây, mình đã thực hiện điều chỉnh tốc độ của loading chạy từ 0 -> 2 bằng việc sử dụng Animator.

    Custom Animator

    Bài tiếp theo mình sẽ giới thiệu làm cách nào để điều chỉnh các thuộc tính động, sẽ có nhiều cái thú vị đấy, hãy đón đọc bài viết của mình bạn nhé.

    Cảm ơn các bạn đã dành thời gian đọc bài của mình. Rất mong nhận được sự góp ý từ các bạn ^^

  • Lottie on Android (Part 1)

    Lottie on Android (Part 1)

    Xin chào các bạn hi hi, lại là mình đây. 🙂
    Để thay đổi không khí sau loạt bài về animation, hôm nay mình sẽ giới thiệu với các bạn về Lottie cho Android.
    Sau những loạt bài về animation thì các bạn có thấy ứng dụng của chúng ta đã trở nên đẹp và sinh động hơn chưa?. Tôi nghĩ chắc chắn là rồi phải không? 🙂
    Nhưng sẽ có nhiều vị khách khó tính thì vẫn có chút xíu chưa hài lòng về độ mượt mà của animation. Bạn đừng lo, Lottie sẽ giải quyết vấn đề đó cho bạn ngay.
    Ứng dụng của bạn sẽ trở nên mượt mà, sinh động và đẹp hơn rất nhiều nữa đấy. Nghe đến đây thì bạn đã hào hứng để tìm hiểu nó rồi chứ. Let’s go…

    Trong bài này mình sẽ giới thiệu đến các bạn những mục sau:

    • Giới thiệu về Lottie
    • Cách sử dụng Lottie cho Android

    Giới thiệu về Lottie

    Lottie là một mã nguồn mở về animation được xây dựng bởi Airbnb. Nó có thể dùng được ở Android(hỗ trợ android từ phiên bản JellyBean API 16), iOS, React Native hay cả Web. Về bản chất hoạt động thì nó sẽ parse animation từ Adobe After Effects, thông qua Bodymovin và được xuất ra định dạng json. Sau đó, các nhà phát triển của các platform sẽ sử dụng công cụ thư viện Lottie để các animation sẽ được hiển thị tương ứng trên các platform.

    Cách sử dụng Lottie cho Android

    Đầu tiên, bạn chuẩn bị cho mình file có định json (được xuất ra từ Adobe After Effects, thông qua Bodymovin như bên trên mình đã chia sẻ).
    Thường thì cái này được design team của các dự án cung cấp cho bạn hoặc bạn tự tạo đều được.
    Bạn cũng có thể tham khảo ở đây rất nhiều.
    Tôi đã chuẩn bị cho mình trail-loading.json cho bài viết này.

    Sau khi có được file định dang json bên trên thì tiếp đến bạn sẽ tạo project và thêm vào trong build.gradle một dependencies như dưới đây:

    dependencies {
        implementation 'com.airbnb.android:lottie:3.4.0'
    }

    Bạn sẽ tạo assets folder (app/src/main/assets) và copy file có định dạng json bên trên vào nhé. Còn mình sẽ copy file trail-loading.json của mình vào.
    Tiếp theo bạn sẽ thêm animation vào xml với layout tương ứng để bạn hiển thị. Ở đây, mình sẽ thêm vào xml trên activity của mình.

    <?xml version="1.0" encoding="utf-8"?>
    <com.airbnb.lottie.LottieAnimationView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/loading_animation"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:lottie_autoPlay="true"
        app:lottie_fileName="trail-loading.json"
        app:lottie_loop="true"
        app:lottie_speed="1" />

    Các thuộc tính mà chúng ta sử dụng bên trên:

    • app:lottie_autoPlay="true": bắt đầu chạy animation.
    • app:lottie_fileName="trail-loading.json": sử dụng file json ở trong thư mục assets mà bạn đã thêm.
    • app:lottie_loop="true": cho phép animation được lặp.
    • app:lottie_speed="1": tốc độ chạy animation.
      Ngoài ra cũng có các thuộc tính sau nữa: lottie_scale, lottie_repeatCount, lottie_repeatMode

    Bên trên là bạn đã thêm animation vào trong xml, nhưng nó cũng có thể được thực hiện, được set các thuộc tính ở onCreate() / onCreateView() bạn nhé.

    package techover.lottie
    
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import kotlinx.android.synthetic.main.activity_main.*
    
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            loading_animation.setAnimation("trail-loading.json")
            loading_animation.loop(true)
            loading_animation.speed = 1f
            loading_animation.playAnimation()
        }
    }

    Thật đơn giản phải không các bạn? Giờ thì xem thành quả của bạn vừa làm nhé.

    Trail-Loading

    Đến đây, các bạn sẽ đặt câu hỏi rằng: tôi có thể điều khiển và lắng nghe nó không? (think)
    Oh, Tất nhiên là được rồi. Bài tiếp theo mình sẽ giới thiệu, hãy đón đọc bài viết của mình bạn nhé.

    Cảm ơn các bạn đã dành thời gian đọc bài của mình. Rất mong nhận được sự góp ý từ các bạn ^^

  • Android Animation (Part 3)

    Android Animation (Part 3)

    Xin chào các bạn, bài tiếp theo trong series về animation của mình đó là giới thiệu cho các bạn về AnimatedVectorDrawable.

    VectorDrawable

    Thông thường để có những hình ảnh tương tích với các màn hình khác nhau trong thiết bị Android thì bạn sẽ tạo ra các hình ảnh như hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi.
    Để tối ưu cho phần này thì trong API 21, Android đã phát hành VectorDrawable giúp thay thế nhiều ảnh .png thành một đồ hoạ vector được tạo bằng xml.
    VectorDrawable bao gồm các điểm, đường thằng, đường cong, màu sắc…khi co giãn không làm ảnh hưởng tới chất lượng của ảnh. Đó thực sự là một điểm mạnh của VectorDrawable.

    Ví dụ VectorDrawable cho numeric_0 ở bài trước như dưới đây:

    <!-- drawable/numeric_0_vector.xml -->
    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24"
        android:viewportHeight="24">
        <path
            android:fillColor="#000"
            android:pathData="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M11,7A2,2 0 0,0 9,9V15A2,2 0 0,0 11,17H13A2,2 0 0,0 15,15V9A2,2 0 0,0 13,7H11M11,9H13V15H11V9Z" />
    </vector>

    cú pháp:
    <vector>: xác định một vector drawable cần vẽ.
    android:width & android:height: kích thước chiều rộng, chiều cao của hình dạng vector.
    android:viewportWidth & android:viewportHeight: khung cửa sổ để vẽ hình dạng vector.
    <path>: bên trong thẻ <vector>, xác định đường dẫn để vẽ.
    android:fillColor: màu bạn sử dụng
    android:pathData: các thuộc tính để vẽ và theo bộ quy tắc dưới đây:

    • M: di chuyển điểm vẽ đến tọa độ x, y (M x y).
    • L: vẽ từ điểm hiện tại đến điểm x, y (L x y).
    • H: vẽ đường ngang từ điểm hiện tại đến điểm có tọa độ x (H x).
    • V: vẽ đường thẳng đứng đến điểm có tọa độ y (V y).
    • C: vẽ đường cong cubic-bezier từ điểm hiện tại x0, y0 đến điểm x, y. Điểm đầu đường cong tiếp tuyến với đường thẳng x0,y0, x1, y1. Điểm thứ 2 của đường cong tiếp tuyến với tường x,y, x2, y2 C x1 y1, x2 y2, x, y.
    • S: vẽ đường cong trơn từ điểm hiện tại x0, y0 đến điểm x, y trong đó điểm đầu tiếp tuyến với đường x0,y0, x2, y2 S x2 y2, x y.
    • Q: vẽ đường cong cubic-bezier từ điểm hiện tại x0, y0 đến điểm x, y. điểm đầu đường cong tiếp tuyến với đường thẳng x0,y0, x1, y1 điểm thứ 2 của đường cong tiếp tuyến với tường x,y, x1, y1 C x1 y1, x y.
    • T: vẽ đường cong cubic-bezier, từ điểm hiện tại đến điểm x,y (T x y).
    • A: vẽ cung tròn.
    • Z: đóng đường vẽ.
    • Ngoài ra, chúng ta còn các cú pháp khác nữa. Bạn có thể tham khảo ở đây

    AnimatedVectorDrawable

    Để giới thiệu cho các bạn về AnimatedVectorDrawable thì tôi sẽ làm giống animation của bài trước, nhưng sẽ sử dụng AnimatedVectorDrawable để cho các bạn hình dung về nó dễ hơn.
    Như bài trước thì chúng ta cần 3 ảnh png: ic_numeric_0.png, ic_numeric_1.png, ic_numeric_2.png và các ảnh cho các màn hình khác nhau trong Android: hdpi, mhdpi, xhdpi, xxhdpi, xxxhdpi là có tới khoảng 15 hình ảnh tất cả phải không?.
    Ở bài này thì các bạn cần chuẩn bị 3 ảnh VectorDrawable cho mình nhé.
    Bạn có thể tìm kiếm ở đây .
    Mình sẽ sử dụng 3 ảnh VectorDrawable của bài trước.

    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:height="24dp"
        android:width="24dp"
        android:viewportWidth="24"
        android:viewportHeight="24">
        <path android:fillColor="#000" android:pathData="M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3H19M11,7A2,2 0 0,0 9,9V15A2,2 0 0,0 11,17H13A2,2 0 0,0 15,15V9A2,2 0 0,0 13,7H11M11,9H13V15H11V9Z" />
    </vector>
    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:height="24dp"
        android:width="24dp"
        android:viewportWidth="24"
        android:viewportHeight="24">
        <path android:fillColor="#000" android:pathData="M14,17H12V9H10V7H14M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" />
    </vector>
    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:height="24dp"
        android:width="24dp"
        android:viewportWidth="24"
        android:viewportHeight="24">
        <path android:fillColor="#000" android:pathData="M15,11C15,12.11 14.1,13 13,13H11V15H15V17H9V13C9,11.89 9.9,11 11,11H13V9H9V7H13A2,2 0 0,1 15,9M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" />
    </vector>

    Chú ý: AnimatedVectorDrawable yêu cầu bạn sử dụng các hình ảnh tương thích với nhau (bạn hiểu đơn giản là android:pathData của các hình ảnh phải có cùng các lệnh, theo cùng một thứ tự và có cùng số lượng tham số cho mỗi lệnh…)
    Các ảnh bên trên sẽ không tương thích nên bài viết này mình sẽ sử dụng ShapeShifter là một ứng dụng web được tạo bởi Alex Lockwood, giúp cho các ảnh .svg tương thích với nhau.
    Sau khi mình sử dụng ShapeShifter thì sẽ được kết qủa như dưới đây animated_vector.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:aapt="http://schemas.android.com/aapt">
        <aapt:attr name="android:drawable">
            <vector
                android:name="vector"
                android:width="96dp"
                android:height="96dp"
                android:viewportWidth="24"
                android:viewportHeight="24">
                <path
                    android:name="path"
                    android:fillColor="#000"
                    android:pathData="M 19 3 C 19.53 3 20.039 3.211 20.414 3.586 C 20.789 3.961 21 4.47 21 5 L 21 19 C 21 19.53 20.789 20.039 20.414 20.414 C 20.039 20.789 19.53 21 19 21 L 5 21 C 4.47 21 3.961 20.789 3.586 20.414 C 3.211 20.039 3 19.53 3 19 L 3 5 C 3 4.47 3.211 3.961 3.586 3.586 C 3.961 3.211 4.47 3 5 3 L 19 3 M 11 7 C 10.47 7 9.961 7.211 9.586 7.586 C 9.211 7.961 9 8.47 9 9 L 9 15 C 9 15.53 9.211 16.039 9.586 16.414 C 9.961 16.789 10.47 17 11 17 L 13 17 C 13.53 17 14.039 16.789 14.414 16.414 C 14.789 16.039 15 15.53 15 15 L 15 9 C 15 8.47 14.789 7.961 14.414 7.586 C 14.039 7.211 13.53 7 13 7 L 11 7 M 11 9 L 13 9 L 13 15 L 11 15 L 11 9 Z" />
            </vector>
        </aapt:attr>
        <target android:name="path">
            <aapt:attr name="android:animation">
                <set>
                    <objectAnimator
                        android:duration="1000"
                        android:interpolator="@android:interpolator/fast_out_slow_in"
                        android:propertyName="pathData"
                        android:valueFrom="M 19 3 C 19.53 3 20.039 3.211 20.414 3.586 C 20.789 3.961 21 4.47 21 5 L 21 19 C 21 19.53 20.789 20.039 20.414 20.414 C 20.039 20.789 19.53 21 19 21 L 5 21 C 4.47 21 3.961 20.789 3.586 20.414 C 3.211 20.039 3 19.53 3 19 L 3 5 C 3 4.47 3.211 3.961 3.586 3.586 C 3.961 3.211 4.47 3 5 3 L 19 3 M 11 7 C 10.47 7 9.961 7.211 9.586 7.586 C 9.211 7.961 9 8.47 9 9 L 9 15 C 9 15.53 9.211 16.039 9.586 16.414 C 9.961 16.789 10.47 17 11 17 L 13 17 C 13.53 17 14.039 16.789 14.414 16.414 C 14.789 16.039 15 15.53 15 15 L 15 9 C 15 8.47 14.789 7.961 14.414 7.586 C 14.039 7.211 13.53 7 13 7 L 11 7 M 11 9 L 13 9 L 13 15 L 11 15 L 11 9 Z"
                        android:valueTo="M 19 3 C 19.53 3 20.039 3.211 20.414 3.586 C 20.789 3.961 21 4.47 21 5 L 21 19 C 21 19.53 20.789 20.039 20.414 20.414 C 20.039 20.789 19.53 21 19 21 L 5 21 C 4.47 21 3.961 20.789 3.586 20.414 C 3.211 20.039 3 19.53 3 19 L 3 5 C 3 4.47 3.211 3.961 3.586 3.586 C 3.961 3.211 4.47 3 5 3 L 19 3 M 11 7 C 10.47 7 9.961 7.211 9.586 7.586 C 9.211 7.961 9 8.47 9 9 L 9 15 C 9 15.53 9.211 16.039 9.586 16.414 C 9.961 16.789 10.47 17 11 17 L 13 17 C 13.53 17 14.039 16.789 14.414 16.414 C 14.789 16.039 15 15.53 15 15 L 15 9 C 15 8.47 14.789 7.961 14.414 7.586 C 14.039 7.211 13.53 7 13 7 L 11 7 M 11 9 L 13 9 L 13 15 L 11 15 L 11 9 Z"
                        android:valueType="pathType" />
                    <objectAnimator
                        android:duration="1000"
                        android:interpolator="@android:interpolator/fast_out_slow_in"
                        android:propertyName="pathData"
                        android:startOffset="1000"
                        android:valueFrom="M 14 17 L 12 17 L 12 9 L 10 9 L 10 7 L 14 7 M 19 3 L 5 3 C 4.47 3 3.961 3.211 3.586 3.586 C 3.211 3.961 3 4.47 3 5 L 3 19 C 3 19.53 3.211 20.039 3.586 20.414 C 3.961 20.789 4.47 21 5 21 L 19 21 C 19.53 21 20.039 20.789 20.414 20.414 C 20.789 20.039 21 19.53 21 19 L 21 5 C 21 4.47 20.789 3.961 20.414 3.586 C 20.039 3.211 19.53 3 19 3 Z"
                        android:valueTo="M 14 17 L 12 17 L 12 9 L 10 9 L 10 7 L 14 7 M 19 3 L 5 3 C 4.47 3 3.961 3.211 3.586 3.586 C 3.211 3.961 3 4.47 3 5 L 3 19 C 3 19.53 3.211 20.039 3.586 20.414 C 3.961 20.789 4.47 21 5 21 L 19 21 C 19.53 21 20.039 20.789 20.414 20.414 C 20.789 20.039 21 19.53 21 19 L 21 5 C 21 4.47 20.789 3.961 20.414 3.586 C 20.039 3.211 19.53 3 19 3 Z"
                        android:valueType="pathType" />
                    <objectAnimator
                        android:duration="1000"
                        android:interpolator="@android:interpolator/fast_out_slow_in"
                        android:propertyName="pathData"
                        android:startOffset="2000"
                        android:valueFrom="M 15 11 C 15 12.11 14.1 13 13 13 L 11 13 L 11 15 L 15 15 L 15 17 L 9 17 L 9 13 C 9 11.89 9.9 11 11 11 L 13 11 L 13 9 L 9 9 L 9 7 L 13 7 C 13.53 7 14.039 7.211 14.414 7.586 C 14.789 7.961 15 8.47 15 9 M 19 3 L 5 3 C 4.47 3 3.961 3.211 3.586 3.586 C 3.211 3.961 3 4.47 3 5 L 3 19 C 3 19.53 3.211 20.039 3.586 20.414 C 3.961 20.789 4.47 21 5 21 L 19 21 C 19.53 21 20.039 20.789 20.414 20.414 C 20.789 20.039 21 19.53 21 19 L 21 5 C 21 4.47 20.789 3.961 20.414 3.586 C 20.039 3.211 19.53 3 19 3 Z"
                        android:valueTo="M 15 11 C 15 12.11 14.1 13 13 13 L 11 13 L 11 15 L 15 15 L 15 17 L 9 17 L 9 13 C 9 11.89 9.9 11 11 11 L 13 11 L 13 9 L 9 9 L 9 7 L 13 7 C 13.53 7 14.039 7.211 14.414 7.586 C 14.789 7.961 15 8.47 15 9 M 19 3 L 5 3 C 4.47 3 3.961 3.211 3.586 3.586 C 3.211 3.961 3 4.47 3 5 L 3 19 C 3 19.53 3.211 20.039 3.586 20.414 C 3.961 20.789 4.47 21 5 21 L 19 21 C 19.53 21 20.039 20.789 20.414 20.414 C 20.789 20.039 21 19.53 21 19 L 21 5 C 21 4.47 20.789 3.961 20.414 3.586 C 20.039 3.211 19.53 3 19 3 Z"
                        android:valueType="pathType" />
                </set>
            </aapt:attr>
        </target>
    </animated-vector>

    Bài viết tiếp theo mình sẽ giới thiệu kỹ hơn đến các bạn về ShapeShifter.
    Còn giờ thì hãy chạy để xem kết quả của bạn vừa làm với những dòng code dưới đây:

    val animatedVectorDrawableCompat =
                AnimatedVectorDrawableCompat.create(this, R.drawable.animated_vector)
            val imageView = (findViewById<ImageView>(R.id.numeric_image)).apply {
                setImageDrawable(animatedVectorDrawableCompat)
            }
            animatedVectorDrawableCompat?.registerAnimationCallback(object :
                Animatable2Compat.AnimationCallback() {
                override fun onAnimationEnd(drawable: Drawable?) {
                    imageView.post { animatedVectorDrawableCompat.start() }
                }
            })
            animatedVectorDrawableCompat?.start()
    AnimatedVectorDrawable

    Mình rất mong được các bạn đón đọc và để lại lời bình luận để mình cải thiện hơn nữa <3

    Link tham khảo:

    • https://developer.android.com/reference/android/graphics/drawable/VectorDrawable
    • https://developer.android.com/guide/topics/graphics/vector-drawable-resources
  • Android Animation (Part 2)

    Android Animation (Part 2)

    Xin chào các bạn, bài tiếp theo trong series về animation của mình đó là FrameAnimation.

    Như ở bài trước mình đã nói thì FrameAnimation là khởi tạo một animation bằng cách sử dụng một chuỗi các hình ảnh được hiển thị theo một thứ tự nhất định với AnimationDrawable. Chúng ta cùng đi tìm hiểu về nó. Les’t go…

    Với định nghĩa về FrameAnimation bên trên thì chúng ta sẽ hình dung là trước tiên chúng ta cần tạo file xml để định nghĩa các frame.
    Bạn có thể tham khảo file numeric_animation.xml của tôi dưới đây:

     <!-- Animation frames are ic_numeric_0.png through ic_numeric_2.png
         files inside the res/drawable/ folder -->
     <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/selected"
        android:oneshot="false">
        <item
            android:drawable="@drawable/ic_numeric_0"
            android:duration="1000" />
        <item
            android:drawable="@drawable/ic_numeric_1"
            android:duration="1000" />
        <item
            android:drawable="@drawable/ic_numeric_2"
            android:duration="1000" />
    </animation-list>

    syntax:

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot=["true" | "false"] >
        <item
            android:drawable="@[package:]drawable/drawable_resource_name"
            android:duration="integer" />
    </animation-list>

    element:
    <animation-list>: là thẻ bắt buộc, nó chứa một hoặc nhiều thẻ <item>.
    android:oneshot: kiểu giá trị kiểu Boolean. “true” là khi bạn muốn thực hiện 1 lần duy nhất. “false” là khi bạn muốn lặp lại.
    <item>: là 1 item trong danh sách, bạn hiểu nó là 1 khung hình khi chạy.
    android:drawable: Drawable của bạn.
    android:duration: kiểu giá trị Integer, nó là thời lượng hiển thị được tính bằng milliseconds.
    Chạy nào, chạy nào…

    private lateinit var numericAnimation: AnimationDrawable
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // Load the ImageView that will host the animation and
            // set its background to our AnimationDrawable XML resource.
            findViewById<ImageView>(R.id.numeric_image).apply {
                setBackgroundResource(R.drawable.numeric_animation)
                // Get the background, which has been compiled to an AnimationDrawable object.
                numericAnimation = background as AnimationDrawable
                // Start the animation (looped playback by default).
                numericAnimation.start()
            }
    
        }

    Ở đây thì bạn cũng thấy rằng trên màn hình main của tôi sẽ có 1 ImageView. Sau đó tôi sẽ chạy animation với AnimationDrawable đã được định nghĩa trong numeric_animation.xml.

    FrameAnimation


    Thật đơn giản phải không các bạn? 😀
    Nhưng nó có nhược điểm khi bạn sử dụng nhiều ảnh với animation có độ phức tạp cao sẽ làm tăng kích thước của ứng dụng, hay là bạn sẽ mất khá nhiều thời gian để custom lại animation đó cho phù hợp với bài toán của bạn. Để giải quyết vấn đề này, bài tiếp theo tôi sẽ giới thiệu cho các bạn về AnimatedVectorDrawable, các bạn hãy đón chờ nhé 😀

    Link tham khảo:

    • https://developer.android.com/guide/topics/graphics/drawable-animation
  • Android Animation (Part 1)

    Android Animation (Part 1)

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

    Tổng quan

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

    Property Animation

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

    View Animation

    View Animation có Tween Animation, Frame Animation.

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

    Tween Animation

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

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

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

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

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

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

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

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

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

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

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

    Link tham khảo:

    • https://developer.android.com/training/animation
  • 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