Hỗ trợ các mật độ pixel khác nhau trong Android

Các thiết bị Android không chỉ khác nhau về kích cỡ màn hình mà bản thân các màn hình còn khác nhau về kích thước pixel. Điều này xảy ra khi giả sử bạn có một thiết bị có 160 pixels trên mỗi inch màn hình và một thiết bị khác có tới 480 pixels trên mỗi inch màn hình. Nếu các bạn bỏ qua vấn đề này các bạn có thể gặp phải việc nội dung hiển thị có thể bị mờ hoặc có thể bị sai lệch về kích thước. Để giải quyết vấn đề này Android cung cấp cho chúng ta rất nhiều giải pháp như sử dụng đơn vị đo lường độc lập để tránh phụ thuộc vào pixel, cung cấp các tài nguyên cho các mật độ pixel khác nhau hay định dạng icon có thể mở rộng theo bất kể kích thước nào mà chúng ta sẽ tìm hiểu sau đây.

1 Sử dụng đơn vị đo lường độc lập (density-independent pixels) .

1.1 Cách sử dụng.

  • Vấn đề chúng ta gặp phải là các màn hình có mật độ pixel khác nhau nghĩa là nếu sử dụng pixel để xác định khoảng cách hay kích thước có thể gây ra sự sai lệch vì các màn hình khác nhau có mật độ pixel khác nhau, do đó, cùng một số pixel có thể tương ứng với các kích thước vật lý khác nhau trên các thiết bị khác nhau. Vậy để giải quyết vấn đề này chúng ta sẽ sử dụng một đơn vị khác độc lập hơn là (dp). Một (dp) là một đơn vị pixel ảo, đơn vị này sẽ tương ứng với 1 pixel thật trên màn hình có mật độ điểm ảnh là 160dpi, con ố 160 này được xác định ra để làm cơ sở tính toán giá trị dp (pixel ảo) này. Android sẽ dựa vào giá trị này để chuyển thành giá trị pixel thực thích hợp cho từng mật độ pixel khác nhau. Tuy nhiên với các nội dung dạng văn bản thì kích thước sẽ được xác định theo đơn vị (sp) đơn vị này có giá trị tương đương với (dp) tuy nhiên nó có thể thay đổi theo kích thước nội dung văn bản mà người dùng cài đặt. Hai đơn vị này không được phép sử dụng thay thế cho nhau. Ví dụ khi bạn cần xác định khoảng cách giữa các view chúng ta sử dụng (dp) còn với textsize chúng ta sử dụng (sp):
<Button android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/clickme"
    android:layout_marginTop="20dp" />

-> (dp) ở đây sử dụng cho khoảng cách view (marginTop).

<TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="20sp" />

-> (sp) ở đây sử dụng cho kích cỡ văn bản (textSize).

1.2 Cách chuyển đổi đơn vị.

  • Trong một số trường hợp, bạn sẽ cần thể hiện kích thước trong dp và sau đó chuyển đổi chúng thành pixel. Như đã trình bày ở trên mỗi đơn vị (dp) pixel ảo này sẽ tương ứng với một pixel thật trên màn hình có độ phân giải 160dp vậy ta có công thức chuyển đổi như sau: px = dp * (dpi / 160) Tại sao lại cần biết đến sự chuyển đổi này, ví dụ ứng dụng của bạn có hỗ trợ thao tác cử chỉ vuốt và bạn cài đặt khi ngón tay người dùng di chuyển 16 pixel thì thao tác này sẽ được thực hiện. Với người dùng sử dụng thiết bị có mật độ điểm ảnh là 160dpi thì việc này tương ứng với việc vuốt trên 1/10 inch màn hình, tuy nhiên với màn hình có mật độ cao hợ ví dụ như 480dpi chẳng hạn thì khoảng cách này chỉ còn 1/3 và nó sẽ không còn giống cử chỉ vuốt cho lắm đối với người dùng dẫn đến trải nghiệm không tốt cho người dùng. Đê tránh sự cố này các thao tác phải được cài đặt bằng (dp) sau đó tính ra giá trị pixel thực với mỗi màn hình:
// Ngưỡng cử chỉ được cài đặt với đơn vị (dp)
private const val GESTURE_THRESHOLD_DP = 16.0f
...
// Ngưỡng cử chỉ được tính với đơn vị pixel
private var mGestureThreshold: Int = 0
...
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Lấy tỷ lệ mật độ của màn hình (bằng 1 với màn hình 160dpi)
    val scale: Float = resources.displayMetrics.density
    // Convert the dps to pixels, based on density scale
    mGestureThreshold = (GESTURE_THRESHOLD_DP * scale + 0.5f).toInt()
}

1.3 Sử dụng các giá trị đã được chia tỷ lệ trước.

  • Bạn có thể sử dụng ViewConfiguration để lấy thông tin các giá trị cơ bản mà hệ điều hành Android sẽ dử dụng. Ví dụ để lấy khoảng cách bằng pixel của ngưỡng cử chỉ cuộn các bạn có thể sử dụng: private val GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).scaledTouchSlop
    Phương thức này sẽ trả về giá trị pixel (Int) mà hệ thống nhận định là người dùng đang thao tác vuốt, giá trị này đúng với tất cả các giá trị mật độ điểm ảnh khác nhau.

2 Sử dụng các tài nguyên Bitmap thay thế.

  • Để cung cấp chất lượng hiển thị là như nhau ở trên các thiết bị có mật độ điểm ảnh khác nhau bạn nên cung cấp các bitmap thay thế tương ứng với từng nhóm mật độ điểm ảnh đã có sẵn trong resource:
  • Ngoài các giá trị trên chúng ta còn có các giá trị:
  • ldpi - Cho các màn hình có độ phân giải cực thấp (120dpi) mà hiện nay gần như không còn thiết bị nào loại này, bạn có thể bỏ qua giá trị này
  • nodpi - Giá trị mà sẽ được sử dụng cho tất cả các mật độ.
  • tvdpi - Giá trị nằm giữa mdpi và hdpi được xác định vào khoảng 213dpi, giá trị này được cài đặt khi bạn muốn ứng dụng hỗ trợ cho tivi.
  • Để tạo các bitmap thay thế cho các mật độ khác nhau, bạn nên tuân theo tỷ lệ tỷ lệ 3: 4: 6: 8: 12: 16 giữa sáu mật độ chính. Ví dụ: nếu bạn có một bitmap là 48x48 pixel cho màn hình mật độ trung bình, tất cả các kích thước khác còn lại sẽ là:
 + 36x36 (0.75x) for low-density (ldpi)
 + 48x48 (1.0x baseline) for medium-density (mdpi)
 + 72x72 (1.5x) for high-density (hdpi)
 + 96x96 (2.0x) for extra-high-density (xhdpi)
 + 144x144 (3.0x) for extra-extra-high-density (xxhdpi)
 + 192x192 (4.0x) for extra-extra-extra-high-density (xxxhdpi)

Các bitmap này sẽ được đặt trong các thư mục tương ứng tại resouce mà khi bạn tạo project đã được khởi tạo sẵn:

res/
  drawable-xxxhdpi/
    resouce-image.png
  drawable-xxhdpi/
    resouce-image.png
  drawable-xhdpi/
    resouce-image.png
  drawable-hdpi/
    resouce-image.png
  drawable-mdpi/
    resouce-image.png
  • Từ đó về sau bạn chỉ cần gọi @drawable/resouce-image thì hệ thống sẽ tự động lấy ảnh tương ứng với mật độ màn hình hiện tại để hiển thị.
  • Ngoài ra còn một lưu ý là các biểu tượng icon của ứng dụng cũng cần được cung cấp cho các mật độ màn hình khác nhau, đôi khi các biểu tượng này sẽ to hơn 25% so với giá trị tỉ lệ của resouce tùy vào launcher mà bạn sử dụng vậy nên bạn cũng cần cung cấp đầy đủ các giá trị tỉ lệ này cho ứng dụng của bạn.
res/
  mipmap-xxxhdpi/
    launcher-icon.png
  mipmap-xxhdpi/
    launcher-icon.png
  mipmap-xhdpi/
    launcher-icon.png
  mipmap-hdpi/
    launcher-icon.png
  mipmap-mdpi/
    launcher-icon.png

3. Sử dụng đồ họa vector.

  • Có một cách khác để tạo ra nhiều phiên bản hình ảnh ứng với nhiều mật độ màn hình khác nhau đó là sử dụng đồ họa vector, tuy vậy đồ họa vector thường được sử dụng cho icon nhiều hơn là cho hình ảnh thông thường. Bạn cũng có thể tạo ra các hình ảnh này bằng các sử dụng công cụ được hỗ trợ sẵn trong Andorid Studio.
    • Ở chế độ xem project click chuột phải và chọn New > Vector Asset.
    • Chọn file (SVG, PSD)
    • Xác định vị trí tệp bạn muốn lưu và thực hiện các điều chỉnh.
  • Khác với đồ họa thông thường đồ họa vector sẽ định nghĩa hình ảnh bằng cách sử dụng file xml để xác định màu sắc và các đường hiển thị của hình ảnh thay vì sử dụng bitmap do đó nó có thể mở rộng theo bất kỳ mật độ điểm ảnh màn hình nào mà không ảnh hưởng đến việc hiển thị.
  • Ví dụ:
<vector android:height="18dp" android:tint="#F04D57"
    android:viewportHeight="24.0" android:viewportWidth="24.0"
    android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#FF000000" android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>
  • Để test việc hiển thị của ứng dụng với các kích cỡ và mật độ hiển thị khác nhau các bạn có thể sử dụng Android Emulator để tạo ra các màn hình với các mật độ hiển thị khác nhau để test.

4. Tổng kết

  • Trên đây mình vừa trình bày sơ lược về hỗ trợ các mật độ pixel khác nhau trong Android các bạn có thể tham khảo và tìm hiểu thêm.
  • Nguồn: https://developer.android.com/guide