layout: post title: Android字符串格式化开源库phrase介绍 category: Android tags: Android keywords: Android,Phrase description: ndroid字符串格式化开源库phrase介绍


引言

Phrase是一个字符串格式化的开源项目,整个项目只有一个类Phrase.java,相比于String.format,通过phrase格式化字符串代码更具可读性。

phrase项目介绍

  • 源码:phrase项目的源代码很简单,里面总共只有一个类:Phrase.java,代码如下:

    package com.eebbk.learnspell.util;
    
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    import android.app.Fragment;
    import android.content.Context;
    import android.content.res.Resources;
    import android.text.SpannableStringBuilder;
    import android.view.View;
    
    public final class Phrase {
      private final CharSequence pattern;
    
      private final Set<String> keys = new HashSet<String>();
      private final Map<String, CharSequence> keysToValues = new HashMap<String, CharSequence>();
    
      private CharSequence formatted;
    
      private Token head;
    
      private char curChar;
      private int curCharIndex;
    
      private static final int EOF = 0;
    
      public static Phrase from(Fragment f, int patternResourceId) {
        return from(f.getResources(), patternResourceId);
      }
    
      public static Phrase from(View v, int patternResourceId) {
        return from(v.getResources(), patternResourceId);
      }
    
      public static Phrase from(Context c, int patternResourceId) {
        return from(c.getResources(), patternResourceId);
      }
    
      public static Phrase from(Resources r, int patternResourceId) {
        return from(r.getText(patternResourceId));
      }
    
      public static Phrase from(CharSequence pattern) {
        return new Phrase(pattern);
      }
    
      public Phrase put(String key, CharSequence value) {
        if (!keys.contains(key)) {
          throw new IllegalArgumentException("Invalid key: " + key);
        }
        if (value == null) {
          throw new IllegalArgumentException("Null value for '" + key + "'");
        }
        keysToValues.put(key, value);
    
        formatted = null;
        return this;
      }
    
      public Phrase put(String key, int value) {
        if (!keys.contains(key)) {
          throw new IllegalArgumentException("Invalid key: " + key);
        }
        keysToValues.put(key, Integer.toString(value));
    
        formatted = null;
        return this;
      }
    
      public Phrase putOptional(String key, CharSequence value) {
        return keys.contains(key) ? put(key, value) : this;
      }
    
      public Phrase putOptional(String key, int value) {
        return keys.contains(key) ? put(key, value) : this;
      }
    
      public CharSequence format() {
        if (formatted == null) {
          if (!keysToValues.keySet().containsAll(keys)) {
            Set<String> missingKeys = new HashSet<String>(keys);
            missingKeys.removeAll(keysToValues.keySet());
            throw new IllegalArgumentException("Missing keys: " + missingKeys);
          }
    
          SpannableStringBuilder sb = new SpannableStringBuilder(pattern);
          for (Token t = head; t != null; t = t.next) {
            t.expand(sb, keysToValues);
          }
    
          formatted = sb;
        }
        return formatted;
      }
    
      @Override
      public String toString() {
        return pattern.toString();
      }
    
      private Phrase(CharSequence pattern) {
        curChar = (pattern.length() > 0) ? pattern.charAt(0) : EOF;
        this.pattern = pattern;
    
        Token prev = null;
        Token next;
        while ((next = token(prev)) != null) {
          if (head == null) head = next;
          prev = next;
        }
      }
    
      private Token token(Token prev) {
        if (curChar == EOF) {
          return null;
        }
        if (curChar == '{') {
          char nextChar = lookahead();
          if (nextChar == '{') {
            return leftCurlyBracket(prev);
          } else if (nextChar >= 'a' && nextChar <= 'z') {
            return key(prev);
          } else {
            throw new IllegalArgumentException(
                "Unexpected character '" + nextChar + "'; expected key.");
          }
        }
        return text(prev);
      }
    
      private KeyToken key(Token prev) {
        StringBuilder sb = new StringBuilder();
    
        consume();
        while ((curChar >= 'a' && curChar <= 'z') || curChar == '_') {
          sb.append(curChar);
          consume();
        }
    
        if (curChar != '}') {
          throw new IllegalArgumentException("Missing closing brace: }");
        }
        consume();
    
        if (sb.length() == 0) {
          throw new IllegalArgumentException("Empty key: {}");
        }
    
        String key = sb.toString();
        keys.add(key);
        return new KeyToken(prev, key);
      }
    
      private TextToken text(Token prev) {
        int startIndex = curCharIndex;
    
        while (curChar != '{' && curChar != EOF) {
          consume();
        }
        return new TextToken(prev, curCharIndex - startIndex);
      }
    
      private LeftCurlyBracketToken leftCurlyBracket(Token prev) {
        consume();
        consume();
        return new LeftCurlyBracketToken(prev);
      }
    
      private char lookahead() {
        return curCharIndex < pattern.length() - 1 ? pattern.charAt(curCharIndex + 1) : EOF;
      }
    
      private void consume() {
        curCharIndex++;
        curChar = (curCharIndex == pattern.length()) ? EOF : pattern.charAt(curCharIndex);
      }
    
      private abstract static class Token {
        private final Token prev;
        private Token next;
    
        protected Token(Token prev) {
          this.prev = prev;
          if (prev != null) prev.next = this;
        }
    
        abstract void expand(SpannableStringBuilder target, Map<String, CharSequence> data);
    
        abstract int getFormattedLength();
    
        final int getFormattedStart() {
          if (prev == null) {
            return 0;
          } else {
            return prev.getFormattedStart() + prev.getFormattedLength();
          }
        }
      }
    
      private static class TextToken extends Token {
        private final int textLength;
    
        TextToken(Token prev, int textLength) {
          super(prev);
          this.textLength = textLength;
        }
    
        @Override void expand(SpannableStringBuilder target, Map<String, CharSequence> data) {
    
        }
    
        @Override int getFormattedLength() {
          return textLength;
        }
      }
    
      private static class LeftCurlyBracketToken extends Token {
        LeftCurlyBracketToken(Token prev) {
          super(prev);
        }
    
        @Override void expand(SpannableStringBuilder target, Map<String, CharSequence> data) {
          int start = getFormattedStart();
          target.replace(start, start + 2, "{");
        }
    
        @Override int getFormattedLength() {
          return 1;
        }
      }
    
      private static class KeyToken extends Token {
        private final String key;
    
        private CharSequence value;
    
        KeyToken(Token prev, String key) {
          super(prev);
          this.key = key;
        }
    
        @Override void expand(SpannableStringBuilder target, Map<String, CharSequence> data) {
          value = data.get(key);
    
          int replaceFrom = getFormattedStart();
          int replaceTo = replaceFrom + key.length() + 2;
          target.replace(replaceFrom, replaceTo, value);
        }
    
        @Override int getFormattedLength() {
          return value.length();
        }
      }
    }
    
  • 字符串格式化原理
    通过阅读Phrase.java的代码可知,它用"{"和"}"将需要格式化的内容包起来,然后用键值对给需要改变的内容传值,包起来的内容为键,值为动态设置的内容,比如:

    "Hi {first_name}, you are {age} years old."
    

我们要最终的显示内容为:“Hi UperOne, you are 26 years old.”这里的first_name和age是键,值为UperOne和26。

使用方法

使用起来很简单,通过键值对的方式插值:

    CharSequence parseStr = Phrase.from("Hi {first_name}, you are {age} years old.")
                     .put("first_name", "UperOne")
                     .put("age", "26")
                     .format();

    mParseTxt.setText( parseStr );