Support multi screen android
Bài đăng này đã không được cập nhật trong 8 năm
I. Mở đầu
1. Nguyên nhân
-
Có thể có nhiều bạn hỏi tại sao tôi lại đưa ra bài viết này, thật đơn giản vì android là một hệ sinh thái mở, mà đã mở thì nguy cơ phân mảnh vô cùng cao cho nên không có giới hạn tiêu chuẩn nào về màn hình, bố cục ứng dụng cả.
-
Về phân mảnh thì các bạn có thể tham khảo bài phân tích từ http://vnreview.vn/tin-tuc-thi-truong/-/view_content/content/1300415/android-dang-bi-phan-manh-boi-gan-20-000-loai-thiet-bi-khac-nhau
Vậy làm sao để giúp các bạn lập trình viên tốn ít công nhất khi phát triển ứng dụng mà vẫn đáp ứng được các loại màn hình lớn, bé khác nhau của android. Sau khi tham khảo google, và một số bài viết được share trên google+ tôi cũng đã thử lò mò để làm một tool nho nhỏ vừa là phục vụ cho mình vừa là chia sẻ cộng đồng để mọi người cùng sử dụng và đánh giá.
2. Ý tưởng
Là một người thích làm các tool để trick để giảm công sức tay to nên ban đầu mình định viết tool bằng java-mx hoặc swing nhưng thấy nó tốn khá nhiều công, và sau khi lần mò mình chọn luôn gradle, được hỗ trợ sẵn với android studio.
Để sử dụng command gradle trong android studio thì ở phần terminal bạn có thể đánh lệnh như cú pháp:
./gradlew GRADLE_COMMAND
(ubuntu, macos)
gradlew GRADLE_COMMAND
(windows)
Tool này sẽ hỗ trợ build file dimension từ:
- file layout (bạn có thể hardecode thoải mái khi lập trinh cho tiện preview)
- từ 1 file dimension có sẵn (ví dụ bạn định nghĩa các giá trị này ở values mặc định, và nó chỉ support device test mà bạn đang làm)
- tự động tạo ra 1 loạt các giá trị dimensions trong giải giá trị mà bạn mong muốn (ví dụ padding. margin thì từ 0->20dp, cỡ chữ thì từ 10sp->30sp) chẳng hạn
II. Coding
1. Sử dụng gradle
Bạn sẽ thấy trong project của bạn (không chơi các project eclipse đâu nhóe) có các file build.gradle, và các file này sẽ hỗ trợ build ứng dụng, setting thư viện các kiểu cho các bạn. Vậy mình sẽ hướng dẫn các bạn sử dụng file này để làm tool theo ý của mình luôn.
Tùy vào chức năng thì bạn có thể tạo ra các file .gradle tương ứng cho dễ quản lý nhé, ở project này mình đặt tên là autodimension.gradle
Bạn đặt file này ở trong thư mục gốc của app và module nào sử dụng thì bạn thêm:
apply from: '../autodimension.gradle'
Còn nếu bạn đặt ở trong module thì chỉ cần thêm
apply from: 'autodimension.grale'
2. "Cột" chức năng
Bạn copy nội dung dưới đây vào file autodimension.gradle ở trên (lưu ý là bạn có thể code trong gralde file như 1 class java nhé)
public class DimenFactory extends DefaultTask {
`@Input
int[] dimens = [320, 360, 384, 411, 480, 540, 600, 720, 800, 1024, 1080, 1280, 1440, 2560, 3840];
`@Input
int fromDimen = 300;
`@Input
double positiveMaxDP = 200
`@Input
double positiveMaxSP = 60
`@Input
double negativeMaxDP = 60
`@Input
double negativeMaxSP = 20
int maxSize = 810;
`@Input
DimenType type = DimenType.FROM_LAYOUT
`@Input
String dimenFileName = "values/dimen.xml"
String resFolder = project.getProjectDir().getPath() + "/src/main/res/"
String layoutFolder = project.getProjectDir().getPath() + "/src/main/res/layout"
String layoutLandFolder = project.getProjectDir().getPath() + "/src/main/res/layout-land"
`@TaskAction
def create() {
switch (type) {
case DimenType.FROM_LAYOUT:
createDimenFromLayout();
break;
case DimenType.AUTO_CREATE:
autoCreateDimen();
break;
case DimenType.FROM_DIMEN_FILE:
createDimenFromDimenFile();
break;
default:
break;
}
}
}
Các bạn nhớ bỏ cái dấú (`) trước @ đi nhé, không hiểu sao blog này lại không support @@
Vậy là bạn đã thấy 3 chức năng mình định nghĩa ở trên rồi đúng không, còn @ Input là để định nghĩa các tham số truyền vào như khi gọi hàm java vậy đó
@ TaskAction là dùng để định nghĩa đây là một task để gradle chạy
3. Tạo dimension từ layout file
def createDimenFromLayout() {
Map<String, String> map = new HashMap<>();
addToMap(layoutFolder, map);
//addToMap(layoutLandFolder)
Set<String> holder = new HashSet<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
Set<String> childHolder = new HashSet<>();
String val = entry.getValue();
getDpValues(val, childHolder);
holder.addAll(childHolder);
for (String s : childHolder) {
int valInt = java.lang.Integer.parseInt(s.replaceAll("sp", "").replaceAll("dp", ""));
if (s.contains("dp")) {
if (valInt < 0) {
val = val.replaceAll(s, "@dimen/dp_minus" + (-valInt));
} else {
val = val.replaceAll(s, "@dimen/dp_" + valInt);
}
} else if (s.contains("sp")) {
if (valInt < 0) {
val = val.replaceAll(s, "@dimen/sp_minus" + (-valInt));
} else {
val = val.replaceAll(s, "@dimen/sp_" + valInt);
}
}
}
saveToFile(entry.getKey(), val);
}
Set<Integer> dps = new HashSet<>();
Set<Integer> sps = new HashSet<>();
for (String s : holder) {
if (s.contains("dp")) {
dps.add(java.lang.Integer.valueOf(s.replaceAll("dp", "")));
} else if (s.contains("sp")) {
sps.add(java.lang.Integer.valueOf(s.replaceAll("sp", "")));
}
}
LinkedList<Integer> listDps = new ArrayList<>(dps);
Collections.sort(listDps, new Comparator<Integer>() {
@Override
int compare(Integer i1, Integer i2) {
return i1 - i2;
}
});
LinkedList<Integer> listSps = new ArrayList<>(sps);
Collections.sort(listSps, new Comparator<Integer>() {
@Override
int compare(Integer i1, Integer i2) {
return i1 - i2;
}
});
for (int dimen : dimens) {
String folder = resFolder + "values-sw" + (int) dimen + "dp";
String fileName = folder + "/auto_dimens.xml";
new File(folder).mkdirs();
new File(fileName).createNewFile();
PrintWriter printWriter = new PrintWriter(fileName);
printWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
printWriter.println("<resources>");
for (int i = 0; i < listDps.size(); i++) {
int iVal = listDps.get(i);
double ratio = iVal / fromDimen;
double sdp = ratio * dimen;
if (iVal < 0) {
printWriter.printf("\t<dimen name=\"dp_minus%d\">%.2fdp</dimen>\r\n", -iVal, sdp);
} else {
printWriter.printf("\t<dimen name=\"dp_%d\">%.2fdp</dimen>\r\n", iVal, sdp);
}
}
for (int i = 0; i < listSps.size(); i++) {
int iVal = listSps.get(i);
double ratio = iVal / fromDimen;
double sdp = ratio * dimen;
if (iVal < 0) {
printWriter.printf("\t<dimen name=\"sp_minus%d\">%.2fsp</dimen>\r\n", -iVal, sdp);
} else {
printWriter.printf("\t<dimen name=\"sp_%d\">%.2fsp</dimen>\r\n", iVal, sdp);
}
}
printWriter.println("</resources>");
printWriter.close();
}
copyDefault(resFolder + "values-sw" + fromDimen + "dp/auto_dimens.xml")
}
4. Tạo dimension từ một file dimension có sẵn
def createDimenFromDimenFile() {
println "Start convert dimen from value file to other screen size";
String path = resFolder + dimenFileName;
Map<String, String> pairs = new HashMap<>();
try {
File fXmlFile = new File(path);
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(fXmlFile);
doc.getDocumentElement().normalize();
NodeList nList = doc.getElementsByTagName("dimen");
for (int temp = 0; temp < nList.getLength(); temp++) {
Node nNode = nList.item(temp);
if (nNode.getNodeType() == Node.ELEMENT_NODE) {
Element eElement = (Element) nNode;
pairs.put(eElement.getAttribute("name"), eElement.getTextContent())
}
}
Map<String, Double> dps = new HashMap<>();
Map<String, Double> sps = new HashMap<>();
for (Map.Entry<String, String> entry : pairs) {
String mVal = entry.getValue();
mVal = mVal.replaceAll("dp", "").replaceAll("sp", "");
double val = java.lang.Double.valueOf(mVal);
if (entry.getValue().contains("dp")) {
dps.put(entry.getKey(), val);
} else {
sps.put(entry.getKey(), val);
}
}
//Sort
dps = new TreeMap<>(dps);
sps = new TreeMap<>(sps);
for (int dimen : dimens) {
String folder = resFolder + "values-sw" + (int) dimen + "dp";
String fileName = folder + "/auto_dimens.xml";
new File(folder).mkdir();
new File(fileName).createNewFile();
PrintWriter printWriter = new PrintWriter(fileName);
printWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
printWriter.println("<resources>");
for (Map.Entry<String, Double> entry : dps) {
double ratio = entry.getValue() / fromDimen;
double sdp = ratio * dimen;
printWriter.printf("\t<dimen name=\"%s\">%.2fdp</dimen>\r\n", entry.getKey(), sdp);
}
for (Map.Entry<String, Double> entry : sps) {
double ratio = entry.getValue() / fromDimen;
double sdp = ratio * dimen;
printWriter.printf("\t<dimen name=\"%s\">%.2fsp</dimen>\r\n", entry.getKey(), sdp);
}
printWriter.println("</resources>");
printWriter.close();
}
copyDefault(resFolder + "values-sw" + fromDimen + "dp/auto_dimens.xml")
} catch (Exception e) {
println e.getMessage();
}
}
4. Tạo trước một mảng các giá trị dimension
def autoCreateDimen() {
println "Auto create dimension file and values";
for (int dimen : dimens) {
String folder = resFolder + "values-sw" + (int) dimen + "dp";
String fileName = folder + "/auto_dimens_positive.xml";
new File(folder).mkdir();
new File(fileName).createNewFile();
PrintWriter printWriter = new PrintWriter(fileName);
printWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
printWriter.println("<resources>");
for (int i = 1; i <= positiveMaxDP; i++) {
double ratio = i / fromDimen;
double sdp = ratio * dimen;
printWriter.printf("\t<dimen name=\"dp_%d\">%.2fdp</dimen>\r\n", i, sdp);
}
for (int i = 1; i <= positiveMaxSP; i++) {
double ratio = i / dimens[0];
double sdp = ratio * dimen;
printWriter.printf("\t<dimen name=\"sp_%d\">%.2fsp</dimen>\r\n", i, sdp);
}
printWriter.println("</resources>");
printWriter.close();
}
for (int dimen : dimens) {
String folder = resFolder + "values-sw" + (int) dimen + "dp";
String fileName = folder + "/auto_dimens_negative.xml";
new File(folder).mkdir();
new File(fileName).createNewFile();
PrintWriter printWriter = new PrintWriter(fileName);
printWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
printWriter.println("<resources>");
for (int i = 1; i <= negativeMaxDP; i++) {
double ratio = i / fromDimen;
double sdp = ratio * dimen;
printWriter.printf("\t<dimen name=\"dp_minus%d\">%.2fdp</dimen>\r\n", i, -sdp);
}
for (int i = 1; i <= negativeMaxSP; i++) {
double ratio = i / fromDimen;
double sdp = ratio * dimen;
printWriter.printf("\t<dimen name=\"sp_minus%d\">%.2fsp</dimen>\r\n", i, -sdp);
}
printWriter.println("</resources>");
printWriter.close();
}
}
5. Chạy lệnh
Để chạy được lệnh task trên bằng grade thì bạn thêm method:
task createDimen(type: DimenFactory) {}
createDimen {
dimens = [320, 360, 384, 411, 480, 540, 600, 720, 800, 1024, 1080, 1280, 1440, 2560, 3840];
fromDimen = 411
positiveMaxDP = 200 //change to 600 or any other value if needed
positiveMaxSP = 60 //change to 600 or any other value if needed
negativeMaxDP = 60 //change to 600 or any other value if needed
negativeMaxSP = 20 //change to 600 or any other value if needed
type = DimenType.FROM_LAYOUT
dimenFileName = 'values/dimens.xml' // name of file, for type = DimenType.FROM_DIMEN_FILE only
}
Rồi giờ bạn xem trong các task của project thì sẽ thấy ngoài các task build ứng dụng ra có 1 task tên là createDimen nữa là OK
Hoặc ở terminal android studio bạn chạy lệnh:
./gradlew tasks
Ok done. Bạn có thể thay đổi các tham số từ method createDimen ở trên theo ý của bạn rồi run lệnh
./gradlew createDimen
Chờ vài s. vậy là xong, thật đơn giản phải không nào?
III. Demo
Source code: https://github.com/nvmanh/android-auto-generate-dimension Demo App: https://play.google.com/store/apps/details?id=com.nvmanh.multiscreen
Nếu có gì thắc mắc thì đừng ngại hãy gửi email cho mình nhé manhduongvtt@gmail.com
Chúc các bạn vui vẻ.
All rights reserved