GIẢI PHÁP XỬ LÍ NHIỀU MÀN HÌNH TRONG ANDROID – P2

Bài viết này giải thích giải pháp của: Gemserk blog theo cách hiểu của mình.
This post explains Gemserk’s solution: Gemserk blog to handle multiple screen sizes in my own way.


Trong bài này mình muốn các bạn phân biệt rõ 2 khái niệm, đó là RealViewport và VirtualViewport ( VirtualViewport là khái niệm trọng tâm của phương pháp). 2 khái niệm này sẽ được giải thích tại chỗ thích hợp của bài.

Theo Gemserk, chúng ta sẽ xây dựng 3 class đó là: VirtualViewport, MultipleVirtualViewportBuilder, và OrthographicCameraWithVirtualViewport. Vì blog của mình chưa có plugins để chèn code nên các bạn hãy vào blog của Gemserk để đọc code của 3 class này.

MULtiple-virtual-viewport-builder

Class này sẽ chứa thông tin của 2 khoảng kích thước màn hình chúng ta muốn hỗ trợ tốt nhất, theo như bài viết trước, đó là (800 x 480) và (854 x 600), chúng ta lần lượt gán cho các biến (minWidth, minHeight) và (maxWidth, maxHeight) của class này.

Sau đó từ kích thước của thiết bị ( lấy từ Gdx.graphics.getWidth()Gdx.graphics.getHeight()), tương ứng với 2 biến widthheight trong hàm getVirtualViewport(float, foat), nó sẽ tính toán để cho ra kích thước của VirtualViewport (virtualViewportWidth, virtualViewportHeight).

Ghi chú: VirtualViewport là viewport ảo mà chúng ta nên đặt các thành phần động ( như buttons, game title…) ở mép của nó. VirtualViewport sẽ luôn luôn có kích thước nằm trong khoảng của min và max. Và tỉ lệ của VirtualViewport không nhất thiết phải bằng với tỉ lệ của thiết bị.

Giải thích code: sẽ có tất cả 4 trường hợp xảy ra:

1. Nếu widthheight đã nằm gọn trong min và max thì ta sẽ trả về một VirtualViewport với kích thước chính là widthheight luôn.
g2993
Nếu nó không nằm gói gọn trong min và max, thì ta sẽ thử 2 trường hợp tiếp theo, 2 trường hợp này sẽ cho ta tỉ lệ kích thước của VirtualViewport bằng với tỉ lệ của widthheight

2. Cố định virtualViewportWidth theo minWidth
virtualViewportWidth = width * scaleForMinSize = width * (minWidth width) = minWidth.
virtualViewportHeight  = virtualViewportWidth * aspect.
Hàm insideBounds(float, float) cho phép ta kiểm tra xem kích thước VirtualViewport mới tính toán có nằm trong khoảng min và max hay không.
Nếu có thì lúc đó ta sẽ được một VirtualViewport có kích thước là hình chữ nhật nhỏ nhất chứa vùng min, tương tự như thế này:

g2993

Nếu không thì ta đến với trường hợp 3

3. Cố định virtualViewportWidth theo maxWidth:
Như trường hợp 2, giải thích hoàn toàn tương tự.
VirtualViewport lúc này sẽ có dạng như sau:
g2993
Nếu vẫn không nằm trong khoảng min và max thì đến trường hợp 4:

4. Trả về VirtualViewport với kích thước minWidth, minHeight:
VirtualViewport lúc đó chính là hình vuông màu đỏ

Nhận xét:
1. Trong trường hợp 2 và 3, không cố định theo minWidthmaxWidth, mà cố định theo minHeightmaxHeight có được không ? Trả lời: tất nhiên được, code thử xem đê !

2. Trong trường hợp 1, 2 và 3, ta đều nhận được VirtualViewport có tỉ lệ kích thước bằng với tỉ lệ kích thước của thiết bị: virtualViewportWidth / virtualViewportHeight = width / height. Còn trong trường hợp 4 thì không, lúc đó virtualViewportWidth / virtualViewportHeight = minWidth / minHeight

virtual-viewport

Class này có virtualWidthvirtualHeight chính là kích thước của VirtualViewport tính toán được từ class MultipleVirtualViewportBuilder ở trên.
Các hàm getWidth(), getHeight(), sẽ cho ta kích thước của RealViewport, là realWidth và realHeight. Mặc dù trong bài viết của Gemserk không có các tên gọi này nhưng để tiện trong việc giải thích mình đã thêm vào, các bạn chỉ cần biết 2 hàm getWidth()getHeight(), sẽ tả về realWidth và realHeight.

Ghi chú: kích thước của RealViewport chính là kích thước để truyền vào camera, nó chính là 2 biến viewportWidth và viewportHeight của OrthographicCamera mà chúng ta thường dùng. Và kích thước này chính là vùng hiện game lên trên thiết bị của chúng ta. Như vậy, RealViewport phải thỏa mản 2 điều kiện:
+ tỉ lệ realWidth / realHeight phải bằng tỉ lệ của kích thước màn hình thiết bị ( tất nhiên- lúc đó thì hình ảnh game hiện lên trên thiết bị mới giữ đúng tỉ lệ của nó và không bị bóp méo)
+ RealViewport phải chứa VirtualViewport, và kích thước của RealViewport phải làm thế nào đó để nó nhỏ hơn kích thước của vùng max trong càng nhiều trường hợp càng tốt
=> Trường hợp tối ưu đó là: RealViewport phải là hình chữ nhật nhỏ nhất chứa VirtualViewport, và tỉ lệ của RealViewport phải bằng với tỉ lệ của màn hình.

Trước khi đi vào giải thích code, ta xét bài toán sau đây: Cho một hình chữ nhật kích thước bất kì (a, b) và một tỉ lệ k. Ta xây dựng một hình chữ nhật mới (x, y) sao cho nó có tỉ lệ x / y = k, bằng 2 phương pháp sau đây:
Phương pháp 1: Cho x = a => y = x / k = a / k.
Phương pháp 2: Cho y = b => x = y * k = b * k.
Ta có thể chứng minh các điều sau:
+ Luôn luôn có một trường hợp mà ta sẽ nhận được một hình chữ nhật (x, y) có thể chứa hình chữ nhật (a, b). Nếu pp1 tính ra (x, y) không chứa, thì pp2 sẽ chứa – và ngược lại.
+ Lúc đó (x, y) là hình chữ nhật nhỏ nhất chứa (a, b), sao cho x / y = k. ( vì trong 2 phương pháp ta đã gán x hoặc y chính bằng a hoặc b)

Đó chính xác là cách mà Gemserk đã xây dựng RealViewport, (a, b) chính là kích thước của VirtualViewport, k chính là tỉ lệ màn hình aspect = screenWidth / screenHeight, và (x, y) chính là RealViewport cần tìm.

Trong hàm getWidth(), cũng như getHeight(), hãy để ý đến biểu thức điều kiện trong if(…) else, điều kiện đó cũng chính là:
if (aspect >= virtualAspect) {….} else {…}

Do trong nhiều trường hợp ta không thể so sánh chính xác 2 đại lượng float (do cách biểu diễn số thực trong máy tính, google để biết thêm thông tin chi tiết), nên so sánh bằng ở đây được thay bằng một trị tuyệt đối, có nghĩa là: nếu aspect hơn kém virtualAspect một khoảng 0.01f, thì cứ coi như là bằng nhau.

Vậy vì sao phải chia trường hợp như vậy ? Trả lời: để biết là nên dùng phương pháp 1 hay phương pháp 2, trong bài toán ở trên ( xét cho trường hợp a >b sẽ không ảnh hưởng đến tính tổng quát của bài toán), nếu ta có k > a/b thì ta sẽ dùng phương pháp 2, ngược lại thì dùng phương pháp 1, lúc đó ta sẽ nhận được hình chữ nhật thỏa mãn điều kiện. (Phần chứng minh xin dành lại cho bạn đọc).

Cách chia trường hợp như trên có vẻ không tự nhiên để tiếp cận ? Thực ra bạn không cần chia trường hợp như trên, mà chỉ cần code trâu thử lần lượt từng trường hợp (phương pháp) cũng được, nếu làm theo pp1 không thỏa mãn thì làm theo pp2. Ok !

orthographiccamera-with-virtual-viewport

Sửa virtualViewport.getVirtualWidth()  ở hàm update() thành viewportWidth, tương tự cho height

…..

Nhận xét:

// Để đơn giản, Gemserk đã cố định kích thước theo 2 vùng min, max để tìm ra kích thước của VirtualViewport; tại vì khi đã biết một kích thước, và tỉ lệ, thì việc suy ra kích thước còn lại chỉ là một phép toán. Đây chưa phải là cách làm tối ưu ( tất nhiên trình độ của các dev bên Gemserk có thể làm được tốt hơn là cái chắc !), chúng ta có thể xây dựng một thuật toán để nó có thể tìm ra kích thước một vùng VirtualViewport xanh lá cây nằm giữa 2 vùng min max với aspect = width / height đã biết mà không cần dựa vào minWidth hoặc maxWidth như trên. Tức là quy về bài toán tìm (x, y), biết x / y = aspect, minWidth <= x <= maxWidth và minHeight <= y <= maxHeight

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s