Support multi screen android

I. Mở đầu

1. Nguyên nhân

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é [email protected]

Chúc các bạn vui vẻ.