ICU-20099 Implementing Java ListFormatter proposals for ICU 67.

See #904
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java
index 0eb54f5..2249ceb 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedStringBuilder.java
@@ -2,7 +2,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html#License
 package com.ibm.icu.impl;
 
-import java.text.Format.Field;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
@@ -24,23 +23,29 @@
  *
  * @author sffc (Shane Carr)
  */
-public class FormattedStringBuilder implements CharSequence {
+public class FormattedStringBuilder implements CharSequence, Appendable {
 
     /** A constant, empty FormattedStringBuilder. Do NOT call mutative operations on this. */
     public static final FormattedStringBuilder EMPTY = new FormattedStringBuilder();
 
     char[] chars;
-    Field[] fields;
+    Object[] fields;
     int zero;
     int length;
 
+    /** Number of characters from the end where .append() operations insert. */
+    int appendOffset = 0;
+
+    /** Field applied when Appendable methods are used. */
+    Object appendableField = null;
+
     public FormattedStringBuilder() {
         this(40);
     }
 
     public FormattedStringBuilder(int capacity) {
         chars = new char[capacity];
-        fields = new Field[capacity];
+        fields = new Object[capacity];
         zero = capacity / 2;
         length = 0;
     }
@@ -72,7 +77,7 @@
         return chars[zero + index];
     }
 
-    public Field fieldAt(int index) {
+    public Object fieldAt(int index) {
         assert index >= 0;
         assert index < length;
         return fields[zero + index];
@@ -106,11 +111,20 @@
         return this;
     }
 
-    public int appendChar16(char codeUnit, Field field) {
-        return insertChar16(length, codeUnit, field);
+    /**
+     * Sets the index at which append operations insert. Defaults to the end.
+     *
+     * @param index The index at which append operations should insert.
+     */
+    public void setAppendIndex(int index) {
+        appendOffset = length - index;
     }
 
-    public int insertChar16(int index, char codeUnit, Field field) {
+    public int appendChar16(char codeUnit, Object field) {
+        return insertChar16(length - appendOffset, codeUnit, field);
+    }
+
+    public int insertChar16(int index, char codeUnit, Object field) {
         int count = 1;
         int position = prepareForInsert(index, count);
         chars[position] = codeUnit;
@@ -123,8 +137,8 @@
      *
      * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
      */
-    public int appendCodePoint(int codePoint, Field field) {
-        return insertCodePoint(length, codePoint, field);
+    public int appendCodePoint(int codePoint, Object field) {
+        return insertCodePoint(length - appendOffset, codePoint, field);
     }
 
     /**
@@ -132,7 +146,7 @@
      *
      * @return The number of chars added: 1 if the code point is in the BMP, or 2 otherwise.
      */
-    public int insertCodePoint(int index, int codePoint, Field field) {
+    public int insertCodePoint(int index, int codePoint, Object field) {
         int count = Character.charCount(codePoint);
         int position = prepareForInsert(index, count);
         Character.toChars(codePoint, chars, position);
@@ -147,8 +161,8 @@
      *
      * @return The number of chars added, which is the length of CharSequence.
      */
-    public int append(CharSequence sequence, Field field) {
-        return insert(length, sequence, field);
+    public int append(CharSequence sequence, Object field) {
+        return insert(length - appendOffset, sequence, field);
     }
 
     /**
@@ -156,7 +170,7 @@
      *
      * @return The number of chars added, which is the length of CharSequence.
      */
-    public int insert(int index, CharSequence sequence, Field field) {
+    public int insert(int index, CharSequence sequence, Object field) {
         if (sequence.length() == 0) {
             // Nothing to insert.
             return 0;
@@ -175,7 +189,7 @@
      *
      * @return The number of chars added, which is the length of CharSequence.
      */
-    public int insert(int index, CharSequence sequence, int start, int end, Field field) {
+    public int insert(int index, CharSequence sequence, int start, int end, Object field) {
         int count = end - start;
         int position = prepareForInsert(index, count);
         for (int i = 0; i < count; i++) {
@@ -199,7 +213,7 @@
             CharSequence sequence,
             int startOther,
             int endOther,
-            Field field) {
+            Object field) {
         int thisLength = endThis - startThis;
         int otherLength = endOther - startOther;
         int count = otherLength - thisLength;
@@ -224,8 +238,8 @@
      *
      * @return The number of chars added, which is the length of the char array.
      */
-    public int append(char[] chars, Field[] fields) {
-        return insert(length, chars, fields);
+    public int append(char[] chars, Object[] fields) {
+        return insert(length - appendOffset, chars, fields);
     }
 
     /**
@@ -234,7 +248,7 @@
      *
      * @return The number of chars added, which is the length of the char array.
      */
-    public int insert(int index, char[] chars, Field[] fields) {
+    public int insert(int index, char[] chars, Object[] fields) {
         assert fields == null || chars.length == fields.length;
         int count = chars.length;
         if (count == 0)
@@ -253,7 +267,7 @@
      * @return The number of chars added, which is the length of the other {@link FormattedStringBuilder}.
      */
     public int append(FormattedStringBuilder other) {
-        return insert(length, other);
+        return insert(length - appendOffset, other);
     }
 
     /**
@@ -288,6 +302,9 @@
      * @return The position in the char array to insert the chars.
      */
     private int prepareForInsert(int index, int count) {
+        if (index == -1) {
+            index = length;
+        }
         if (index == 0 && zero - count >= 0) {
             // Append to start
             zero -= count;
@@ -309,13 +326,13 @@
         int oldCapacity = getCapacity();
         int oldZero = zero;
         char[] oldChars = chars;
-        Field[] oldFields = fields;
+        Object[] oldFields = fields;
         if (length + count > oldCapacity) {
             int newCapacity = (length + count) * 2;
             int newZero = newCapacity / 2 - (length + count) / 2;
 
             char[] newChars = new char[newCapacity];
-            Field[] newFields = new Field[newCapacity];
+            Object[] newFields = new Object[newCapacity];
 
             // First copy the prefix and then the suffix, leaving room for the new chars that the
             // caller wants to insert.
@@ -408,7 +425,7 @@
         return new String(chars, zero, length);
     }
 
-    private static final Map<Field, Character> fieldToDebugChar = new HashMap<>();
+    private static final Map<Object, Character> fieldToDebugChar = new HashMap<>();
 
     static {
         fieldToDebugChar.put(NumberFormat.Field.SIGN, '-');
@@ -459,17 +476,59 @@
     }
 
     /** @return A new array containing the field values of this string builder. */
-    public Field[] toFieldArray() {
+    public Object[] toFieldArray() {
         return Arrays.copyOfRange(fields, zero, zero + length);
     }
 
     /**
+     * Call this method before using any of the Appendable overrides.
+     *
+     * @param field The field used when inserting strings.
+     */
+    public void setAppendableField(Object field) {
+        appendableField = field;
+    }
+
+    /**
+     * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take
+     * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first.
+     */
+    @Override
+    public Appendable append(CharSequence csq) {
+        assert appendableField != null;
+        insert(length - appendOffset, csq, appendableField);
+        return this;
+    }
+
+    /**
+     * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take
+     * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first.
+     */
+    @Override
+    public Appendable append(CharSequence csq, int start, int end) {
+        assert appendableField != null;
+        insert(length - appendOffset, csq, start, end, appendableField);
+        return this;
+    }
+
+    /**
+     * This method is provided for Java Appendable compatibility. In most cases, please use the append methods that take
+     * a Field parameter. If you do use this method, you must call {@link #setAppendableField} first.
+     */
+    @Override
+    public Appendable append(char c) {
+        assert appendableField != null;
+        insertChar16(length - appendOffset, c, appendableField);
+        return this;
+    }
+
+    /**
      * @return Whether the contents and field values of this string builder are equal to the given chars
      *         and fields.
      * @see #toCharArray
      * @see #toFieldArray
      */
-    public boolean contentEquals(char[] chars, Field[] fields) {
+    public boolean contentEquals(char[] chars, Object[] fields) {
         if (chars.length != length)
             return false;
         if (fields.length != length)
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java
index 8bfe7a5..0fdd3be 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/FormattedValueStringBuilderImpl.java
@@ -8,7 +8,9 @@
 import java.text.Format.Field;
 
 import com.ibm.icu.text.ConstrainedFieldPosition;
+import com.ibm.icu.text.ListFormatter;
 import com.ibm.icu.text.NumberFormat;
+import com.ibm.icu.text.UFormat;
 import com.ibm.icu.text.UnicodeSet;
 
 /**
@@ -24,6 +26,33 @@
  */
 public class FormattedValueStringBuilderImpl {
 
+    /**
+     * Placeholder field used for calculating spans.
+     * Does not currently support nested fields beyond one level.
+     */
+    public static class SpanFieldPlaceholder {
+        public UFormat.SpanField spanField;
+        public Field normalField;
+        public Object value;
+    }
+
+    /**
+     * Finds the index at which a span field begins.
+     *
+     * @param value The value of the span field to search for.
+     * @return The index, or -1 if not found.
+     */
+    public static int findSpan(FormattedStringBuilder self, Object value) {
+        for (int i = self.zero; i < self.zero + self.length; i++) {
+            if (!(self.fields[i] instanceof SpanFieldPlaceholder)) {
+                continue;
+            }
+            if (((SpanFieldPlaceholder) self.fields[i]).value.equals(value)) {
+                return i - self.zero;
+            }
+        }
+        return -1;
+    }
 
     public static boolean nextFieldPosition(FormattedStringBuilder self, FieldPosition fp) {
         java.text.Format.Field rawField = fp.getFieldAttribute();
@@ -78,7 +107,11 @@
         AttributedString as = new AttributedString(self.toString());
         while (nextPosition(self, cfpos, numericField)) {
             // Backwards compatibility: field value = field
-            as.addAttribute(cfpos.getField(), cfpos.getField(), cfpos.getStart(), cfpos.getLimit());
+            Object value = cfpos.getFieldValue();
+            if (value == null) {
+                value = cfpos.getField();
+            }
+            as.addAttribute(cfpos.getField(), value, cfpos.getStart(), cfpos.getLimit());
         }
         return as.getIterator();
     }
@@ -102,15 +135,20 @@
      */
     public static boolean nextPosition(FormattedStringBuilder self, ConstrainedFieldPosition cfpos, Field numericField) {
         int fieldStart = -1;
-        Field currField = null;
+        Object currField = null;
         for (int i = self.zero + cfpos.getLimit(); i <= self.zero + self.length; i++) {
-            Field _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END;
+            Object _field = (i < self.zero + self.length) ? self.fields[i] : NullField.END;
             // Case 1: currently scanning a field.
             if (currField != null) {
                 if (currField != _field) {
                     int end = i - self.zero;
+                    // Handle span fields; don't trim them
+                    if (currField instanceof SpanFieldPlaceholder) {
+                        assert handleSpan(currField, cfpos, fieldStart, end);
+                        return true;
+                    }
                     // Grouping separators can be whitespace; don't throw them out!
-                    if (currField != NumberFormat.Field.GROUPING_SEPARATOR) {
+                    if (isTrimmable(currField)) {
                         end = trimBack(self, end);
                     }
                     if (end <= fieldStart) {
@@ -121,10 +159,10 @@
                         continue;
                     }
                     int start = fieldStart;
-                    if (currField != NumberFormat.Field.GROUPING_SEPARATOR) {
+                    if (isTrimmable(currField)) {
                         start = trimFront(self, start);
                     }
-                    cfpos.setState(currField, null, start, end);
+                    cfpos.setState((Field) currField, null, start, end);
                     return true;
                 }
                 continue;
@@ -154,6 +192,15 @@
                 cfpos.setState(numericField, null, j - self.zero + 1, i - self.zero);
                 return true;
             }
+            // Special case: emit normalField if we are pointing at the end of spanField.
+            if (i > self.zero
+                    && self.fields[i-1] instanceof SpanFieldPlaceholder) {
+                int j = i - 1;
+                for (; j >= self.zero && self.fields[j] == self.fields[i-1]; j--) {}
+                if (handleSpan(self.fields[i-1], cfpos, j - self.zero + 1, i - self.zero)) {
+                    return true;
+                }
+            }
             // Special case: skip over INTEGER; will be coalesced later.
             if (_field == NumberFormat.Field.INTEGER) {
                 _field = null;
@@ -163,7 +210,16 @@
                 continue;
             }
             // Case 3: check for field starting at this position
-            if (cfpos.matchesField(_field, null)) {
+            // Case 3a: SpanField placeholder
+            if (_field instanceof SpanFieldPlaceholder) {
+                SpanFieldPlaceholder ph = (SpanFieldPlaceholder) _field;
+                if (cfpos.matchesField(ph.normalField, null) || cfpos.matchesField(ph.spanField, ph.value)) {
+                    fieldStart = i - self.zero;
+                    currField = _field;
+                }
+            }
+            // Case 3b: No SpanField
+            else if (cfpos.matchesField((Field) _field, null)) {
                 fieldStart = i - self.zero;
                 currField = _field;
             }
@@ -173,14 +229,19 @@
         return false;
     }
 
-    private static boolean isIntOrGroup(Field field) {
+    private static boolean isIntOrGroup(Object field) {
         return field == NumberFormat.Field.INTEGER || field == NumberFormat.Field.GROUPING_SEPARATOR;
     }
 
-    private static boolean isNumericField(Field field) {
+    private static boolean isNumericField(Object field) {
         return field == null || NumberFormat.Field.class.isAssignableFrom(field.getClass());
     }
 
+    private static boolean isTrimmable(Object field) {
+        return field != NumberFormat.Field.GROUPING_SEPARATOR
+                && !(field instanceof ListFormatter.Field);
+    }
+
     private static int trimBack(FormattedStringBuilder self, int limit) {
         return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES)
                 .spanBack(self, limit, UnicodeSet.SpanCondition.CONTAINED);
@@ -190,4 +251,19 @@
         return StaticUnicodeSets.get(StaticUnicodeSets.Key.DEFAULT_IGNORABLES)
                 .span(self, start, UnicodeSet.SpanCondition.CONTAINED);
     }
+
+    private static boolean handleSpan(Object field, ConstrainedFieldPosition cfpos, int start, int limit) {
+        SpanFieldPlaceholder ph = (SpanFieldPlaceholder) field;
+        if (cfpos.matchesField(ph.spanField, ph.value)
+                && cfpos.getLimit() < limit) {
+            cfpos.setState(ph.spanField, ph.value, start, limit);
+            return true;
+        }
+        if (cfpos.matchesField(ph.normalField, null)
+                && (cfpos.getLimit() < limit || cfpos.getField() != ph.normalField)) {
+            cfpos.setState(ph.normalField, null, start, limit);
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java
index 216b58a..8458534c 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/SimpleFormatterImpl.java
@@ -8,6 +8,11 @@
  */
 package com.ibm.icu.impl;
 
+import java.io.IOException;
+import java.text.Format;
+
+import com.ibm.icu.util.ICUUncheckedIOException;
+
 /**
  * Formats simple patterns like "{1} was born in {0}".
  * Internal version of {@link com.ibm.icu.text.SimpleFormatter}
@@ -304,17 +309,124 @@
         return sb.toString();
     }
 
-    /** Poor-man's iterator interface. See ICU-20406. */
-    public static class Int64Iterator {
+    /**
+     * Returns the length of the pattern text with none of the arguments.
+     * @param compiledPattern Compiled form of a pattern string.
+     * @param codePoints true to count code points; false to count code units.
+     * @return The number of code points or code units.
+     */
+    public static int getLength(String compiledPattern, boolean codePoints) {
+        int result = 0;
+        for (int i = 1; i < compiledPattern.length();) {
+            int segmentLength = compiledPattern.charAt(i++) - ARG_NUM_LIMIT;
+            if (segmentLength > 0) {
+                int limit = i + segmentLength;
+                if (codePoints) {
+                    result += Character.codePointCount(compiledPattern, i, limit);
+                } else {
+                    result += (limit - i);
+                }
+                i = limit;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns the length in code units of the pattern text up until the first argument.
+     * @param compiledPattern Compiled form of a pattern string.
+     * @return The number of code units.
+     */
+    public static int getPrefixLength(String compiledPattern) {
+        if (compiledPattern.length() == 1) {
+            return 0;
+        } else if (compiledPattern.charAt(0) == 0) {
+            return compiledPattern.length() - 2;
+        } else if (compiledPattern.charAt(1) <= ARG_NUM_LIMIT) {
+            return 0;
+        } else {
+            return compiledPattern.charAt(1) - ARG_NUM_LIMIT;
+        }
+    }
+
+    /**
+     * Special case for using FormattedStringBuilder with patterns with 0 or 1 argument.
+     *
+     * With 1 argument, treat the current contents of the FormattedStringBuilder between
+     * start and end as the argument {0}. Insert the extra strings from compiledPattern
+     * to surround the argument in the output.
+     *
+     * With 0 arguments, overwrite the entire contents of the FormattedStringBuilder
+     * between start and end.
+     *
+     * @param compiledPattern Compiled form of a pattern string.
+     * @param field Field to use when adding chars to the output.
+     * @param start The start index of the argument already in the output string.
+     * @param end The end index of the argument already in the output string.
+     * @param output Destination for formatted output.
+     * @return Net number of characters added to the formatted string.
+     */
+    public static int formatPrefixSuffix(
+            String compiledPattern,
+            Format.Field field,
+            int start,
+            int end,
+            FormattedStringBuilder output) {
+        int argLimit = getArgumentLimit(compiledPattern);
+        if (argLimit == 0) {
+            // No arguments in compiled pattern; overwrite the entire segment with our string.
+            return output.splice(start, end, compiledPattern, 2, compiledPattern.length(), field);
+        } else {
+            assert argLimit == 1;
+            int suffixOffset;
+            int length = 0;
+            if (compiledPattern.charAt(1) != '\u0000') {
+                int prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
+                length = output.insert(start, compiledPattern, 2, 2 + prefixLength, field);
+                suffixOffset = 3 + prefixLength;
+            } else {
+                suffixOffset = 2;
+            }
+            if (suffixOffset < compiledPattern.length()) {
+                int suffixLength = compiledPattern.charAt(suffixOffset) - ARG_NUM_LIMIT;
+                length += output.insert(end + length, compiledPattern, 1 + suffixOffset,
+                        1 + suffixOffset + suffixLength, field);
+            }
+            return length;
+        }
+    }
+
+    /** Internal iterator interface for maximum efficiency.
+     *
+     * Usage boilerplate:
+     *
+     * <pre>
+     * long state = 0;
+     * while (true) {
+     *     state = IterInternal.step(state, compiledPattern, output);
+     *     if (state == IterInternal.DONE) {
+     *         break;
+     *     }
+     *     int argIndex = IterInternal.getArgIndex(state);
+     *     // Append the string corresponding to argIndex to output
+     * }
+     * </pre>
+     *
+     */
+    public static class IterInternal {
         public static final long DONE = -1;
 
-        public static long step(CharSequence compiledPattern, long state, StringBuffer output) {
+        public static long step(long state, CharSequence compiledPattern, Appendable output) {
             int i = (int) (state >>> 32);
             assert i < compiledPattern.length();
             i++;
             while (i < compiledPattern.length() && compiledPattern.charAt(i) > ARG_NUM_LIMIT) {
                 int limit = i + compiledPattern.charAt(i) + 1 - ARG_NUM_LIMIT;
-                output.append(compiledPattern, i + 1, limit);
+                try {
+                    output.append(compiledPattern, i + 1, limit);
+                } catch (IOException e) {
+                    throw new ICUUncheckedIOException(e);
+                }
                 i = limit;
             }
             if (i == compiledPattern.length()) {
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java
index 39d57c4..d1c0b48 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/ConstantMultiFieldModifier.java
@@ -18,8 +18,8 @@
     // value and is treated internally as immutable.
     protected final char[] prefixChars;
     protected final char[] suffixChars;
-    protected final Field[] prefixFields;
-    protected final Field[] suffixFields;
+    protected final Object[] prefixFields;
+    protected final Object[] suffixFields;
     private final boolean overwrite;
     private final boolean strong;
 
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java
index e0f3be3..6664645 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/CurrencySpacingEnabledModifier.java
@@ -2,8 +2,6 @@
 // License & terms of use: http://www.unicode.org/copyright.html#License
 package com.ibm.icu.impl.number;
 
-import java.text.Format.Field;
-
 import com.ibm.icu.impl.FormattedStringBuilder;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.text.NumberFormat;
@@ -125,7 +123,7 @@
         // NOTE: For prefix, output.fieldAt(index-1) gets the last field type in the prefix.
         // This works even if the last code point in the prefix is 2 code units because the
         // field value gets populated to both indices in the field array.
-        Field affixField = (affix == PREFIX) ? output.fieldAt(index - 1)
+        Object affixField = (affix == PREFIX) ? output.fieldAt(index - 1)
                 : output.fieldAt(index);
         if (affixField != NumberFormat.Field.CURRENCY) {
             return 0;
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java
index 6241848..b80ef77 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/number/SimpleModifier.java
@@ -17,9 +17,6 @@
     private final String compiledPattern;
     private final Field field;
     private final boolean strong;
-    private final int prefixLength;
-    private final int suffixOffset;
-    private final int suffixLength;
 
     // Parameters: used for number range formatting
     private final Parameters parameters;
@@ -39,53 +36,21 @@
         this.field = field;
         this.strong = strong;
         this.parameters = parameters;
-
-        int argLimit = SimpleFormatterImpl.getArgumentLimit(compiledPattern);
-        if (argLimit == 0) {
-            // No arguments in compiled pattern
-            prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
-            assert 2 + prefixLength == compiledPattern.length();
-            // Set suffixOffset = -1 to indicate no arguments in compiled pattern.
-            suffixOffset = -1;
-            suffixLength = 0;
-        } else {
-            assert argLimit == 1;
-            if (compiledPattern.charAt(1) != '\u0000') {
-                prefixLength = compiledPattern.charAt(1) - ARG_NUM_LIMIT;
-                suffixOffset = 3 + prefixLength;
-            } else {
-                prefixLength = 0;
-                suffixOffset = 2;
-            }
-            if (3 + prefixLength < compiledPattern.length()) {
-                suffixLength = compiledPattern.charAt(suffixOffset) - ARG_NUM_LIMIT;
-            } else {
-                suffixLength = 0;
-            }
-        }
     }
 
     @Override
     public int apply(FormattedStringBuilder output, int leftIndex, int rightIndex) {
-        return formatAsPrefixSuffix(output, leftIndex, rightIndex);
+        return SimpleFormatterImpl.formatPrefixSuffix(compiledPattern, field, leftIndex, rightIndex, output);
     }
 
     @Override
     public int getPrefixLength() {
-        return prefixLength;
+        return SimpleFormatterImpl.getPrefixLength(compiledPattern);
     }
 
     @Override
     public int getCodePointCount() {
-        int count = 0;
-        if (prefixLength > 0) {
-            count += Character.codePointCount(compiledPattern, 2, 2 + prefixLength);
-        }
-        if (suffixLength > 0) {
-            count += Character
-                    .codePointCount(compiledPattern, 1 + suffixOffset, 1 + suffixOffset + suffixLength);
-        }
-        return count;
+        return SimpleFormatterImpl.getLength(compiledPattern, true);
     }
 
     @Override
@@ -118,49 +83,6 @@
     }
 
     /**
-     * TODO: This belongs in SimpleFormatterImpl. The only reason I haven't moved it there yet is because
-     * DoubleSidedStringBuilder is an internal class and SimpleFormatterImpl feels like it should not
-     * depend on it.
-     *
-     * <p>
-     * Formats a value that is already stored inside the StringBuilder <code>result</code> between the
-     * indices <code>startIndex</code> and <code>endIndex</code> by inserting characters before the start
-     * index and after the end index.
-     *
-     * <p>
-     * This is well-defined only for patterns with exactly one argument.
-     *
-     * @param result
-     *            The StringBuilder containing the value argument.
-     * @param startIndex
-     *            The left index of the value within the string builder.
-     * @param endIndex
-     *            The right index of the value within the string builder.
-     * @return The number of characters (UTF-16 code points) that were added to the StringBuilder.
-     */
-    public int formatAsPrefixSuffix(
-            FormattedStringBuilder result,
-            int startIndex,
-            int endIndex) {
-        if (suffixOffset == -1) {
-            // There is no argument for the inner number; overwrite the entire segment with our string.
-            return result.splice(startIndex, endIndex, compiledPattern, 2, 2 + prefixLength, field);
-        } else {
-            if (prefixLength > 0) {
-                result.insert(startIndex, compiledPattern, 2, 2 + prefixLength, field);
-            }
-            if (suffixLength > 0) {
-                result.insert(endIndex + prefixLength,
-                        compiledPattern,
-                        1 + suffixOffset,
-                        1 + suffixOffset + suffixLength,
-                        field);
-            }
-            return prefixLength + suffixLength;
-        }
-    }
-
-    /**
      * TODO: Like above, this belongs with the rest of the SimpleFormatterImpl code.
      * I put it here so that the SimpleFormatter uses in FormattedStringBuilder are near each other.
      *
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
index 3e16328..e009d95 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/DateIntervalFormat.java
@@ -1032,11 +1032,11 @@
                 fInfo.getFallbackIntervalPattern(), patternSB, 2, 2);
         long state = 0;
         while (true) {
-            state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo);
-            if (state == SimpleFormatterImpl.Int64Iterator.DONE) {
+            state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo);
+            if (state == SimpleFormatterImpl.IterInternal.DONE) {
                 break;
             }
-            if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) {
+            if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) {
                 if (output != null) {
                     output.register(0);
                 }
@@ -1090,11 +1090,11 @@
             // {1} is single date portion
             long state = 0;
             while (true) {
-                state = SimpleFormatterImpl.Int64Iterator.step(compiledPattern, state, appendTo);
-                if (state == SimpleFormatterImpl.Int64Iterator.DONE) {
+                state = SimpleFormatterImpl.IterInternal.step(state, compiledPattern, appendTo);
+                if (state == SimpleFormatterImpl.IterInternal.DONE) {
                     break;
                 }
-                if (SimpleFormatterImpl.Int64Iterator.getArgIndex(state) == 0) {
+                if (SimpleFormatterImpl.IterInternal.getArgIndex(state) == 0) {
                     fDateFormat.applyPattern(fTimePattern);
                     fallbackFormatRange(fromCalendar, toCalendar, appendTo, patternSB, pos, output, attributes);
                 } else {
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java
index 2162e62..d288e21 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/ListFormatter.java
@@ -8,19 +8,25 @@
  */
 package com.ibm.icu.text;
 
-import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.text.AttributedCharacterIterator;
+import java.text.Format;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.Locale;
 
+import com.ibm.icu.impl.FormattedStringBuilder;
+import com.ibm.icu.impl.FormattedValueStringBuilderImpl;
+import com.ibm.icu.impl.FormattedValueStringBuilderImpl.SpanFieldPlaceholder;
 import com.ibm.icu.impl.ICUCache;
 import com.ibm.icu.impl.ICUData;
 import com.ibm.icu.impl.ICUResourceBundle;
 import com.ibm.icu.impl.SimpleCache;
 import com.ibm.icu.impl.SimpleFormatterImpl;
-import com.ibm.icu.util.ICUUncheckedIOException;
+import com.ibm.icu.impl.SimpleFormatterImpl.IterInternal;
+import com.ibm.icu.impl.Utility;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
 
@@ -41,6 +47,7 @@
 
     /**
      * Indicates the style of Listformatter
+     * TODO(ICU-20888): Remove this in ICU 68.
      * @internal
      * @deprecated This API is ICU internal only.
      */
@@ -99,6 +106,242 @@
     }
 
     /**
+     * Type of meaning expressed by the list.
+     *
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public enum Type {
+        /**
+         * Conjunction formatting, e.g. "Alice, Bob, Charlie, and Delta".
+         *
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        AND,
+
+        /**
+         * Disjunction (or alternative, or simply one of) formatting, e.g.
+         * "Alice, Bob, Charlie, or Delta".
+         *
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        OR,
+
+        /**
+         * Formatting of a list of values with units, e.g. "5 pounds, 12 ounces".
+         *
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        UNITS
+    };
+
+    /**
+     * Verbosity level of the list patterns.
+     *
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public enum Width {
+        /**
+         * Use list formatting with full words (no abbreviations) when possible.
+         *
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        WIDE,
+
+        /**
+         * Use list formatting of typical length.
+         *
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        SHORT,
+
+        /**
+         * Use list formatting of the shortest possible length.
+         *
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        NARROW,
+    };
+
+    /**
+     * Class for span fields in FormattedDateInterval.
+     *
+     * @draft ICU 64
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static final class SpanField extends UFormat.SpanField {
+        private static final long serialVersionUID = 3563544214705634403L;
+
+        /**
+         * The concrete field used for spans in FormattedList.
+         *
+         * Instances of LIST_SPAN should have an associated value, the index
+         * within the input list that is represented by the span.
+         *
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        public static final SpanField LIST_SPAN = new SpanField("list-span");
+
+        private SpanField(String name) {
+            super(name);
+        }
+
+        /**
+         * serizalization method resolve instances to the constant
+         * ListFormatter.SpanField values
+         * @internal
+         * @deprecated This API is ICU internal only.
+         */
+        @Deprecated
+        @Override
+        protected Object readResolve() throws InvalidObjectException {
+            if (this.getName().equals(LIST_SPAN.getName()))
+                return LIST_SPAN;
+
+            throw new InvalidObjectException("An invalid object.");
+        }
+    }
+
+    /**
+     * Field selectors for format fields defined by ListFormatter.
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static final class Field extends Format.Field {
+        private static final long serialVersionUID = -8071145668708265437L;
+
+        /**
+         * The literal text in the result which came from the resources.
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        public static Field LITERAL = new Field("literal");
+
+        /**
+         * The element text in the result which came from the input strings.
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        public static Field ELEMENT = new Field("element");
+
+        private Field(String name) {
+            super(name);
+        }
+
+        /**
+         * Serizalization method resolve instances to the constant Field values
+         *
+         * @draft ICU 64
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        protected Object readResolve() throws InvalidObjectException {
+            if (this.getName().equals(LITERAL.getName()))
+                return LITERAL;
+            if (this.getName().equals(ELEMENT.getName()))
+                return ELEMENT;
+
+            throw new InvalidObjectException("An invalid object.");
+        }
+    }
+
+    /**
+     * An immutable class containing the result of a list formatting operation.
+     *
+     * Instances of this class are immutable and thread-safe.
+     *
+     * Not intended for public subclassing.
+     *
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static final class FormattedList implements FormattedValue {
+        private final FormattedStringBuilder string;
+
+        FormattedList(FormattedStringBuilder string) {
+            this.string = string;
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            return string.toString();
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public int length() {
+            return string.length();
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public char charAt(int index) {
+            return string.charAt(index);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public CharSequence subSequence(int start, int end) {
+            return string.subString(start, end);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public <A extends Appendable> A appendTo(A appendable) {
+            return Utility.appendTo(string, appendable);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public boolean nextPosition(ConstrainedFieldPosition cfpos) {
+            return FormattedValueStringBuilderImpl.nextPosition(string, cfpos, null);
+        }
+
+        /**
+         * {@inheritDoc}
+         * @draft ICU 67
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public AttributedCharacterIterator toCharacterIterator() {
+            return FormattedValueStringBuilderImpl.toCharacterIterator(string, null);
+        }
+    }
+
+    /**
      * <b>Internal:</b> Create a ListFormatter from component strings,
      * with definitions as in LDML.
      *
@@ -145,6 +388,50 @@
      * @param locale
      *            the locale in question.
      * @return ListFormatter
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static ListFormatter getInstance(ULocale locale, Type type, Width width) {
+        String styleName = typeWidthToStyleString(type, width);
+        if (styleName == null) {
+            throw new IllegalArgumentException("Invalid list format type/width");
+        }
+        return cache.get(locale, styleName);
+    }
+
+    /**
+     * Create a list formatter that is appropriate for a locale.
+     *
+     * @param locale
+     *            the locale in question.
+     * @return ListFormatter
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static ListFormatter getInstance(Locale locale, Type type, Width width) {
+        return getInstance(ULocale.forLocale(locale), type, width);
+    }
+
+    /**
+     * Create a list formatter that is appropriate for a locale and style.
+     *
+     * @param locale the locale in question.
+     * @param style the style
+     * @return ListFormatter
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    @Deprecated
+    public static ListFormatter getInstance(ULocale locale, Style style) {
+        return cache.get(locale, style.getName());
+    }
+
+    /**
+     * Create a list formatter that is appropriate for a locale.
+     *
+     * @param locale
+     *            the locale in question.
+     * @return ListFormatter
      * @stable ICU 50
      */
     public static ListFormatter getInstance(ULocale locale) {
@@ -164,20 +451,6 @@
     }
 
     /**
-     * Create a list formatter that is appropriate for a locale and style.
-     *
-     * @param locale the locale in question.
-     * @param style the style
-     * @return ListFormatter
-     * @internal
-     * @deprecated This API is ICU internal only.
-     */
-    @Deprecated
-    public static ListFormatter getInstance(ULocale locale, Style style) {
-        return cache.get(locale, style.getName());
-    }
-
-    /**
      * Create a list formatter that is appropriate for the default FORMAT locale.
      *
      * @return ListFormatter
@@ -208,30 +481,59 @@
      * @stable ICU 50
      */
     public String format(Collection<?> items) {
-        return format(items, -1).toString();
+        return formatImpl(items, false).toString();
+    }
+
+    /**
+     * Format a list of objects to a FormattedList. You can access the offsets
+     * of each element from the FormattedList.
+     *
+     * @param items
+     *            items to format. The toString() method is called on each.
+     * @return items formatted into a FormattedList
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public FormattedList formatToValue(Object... items) {
+        return formatToValue(Arrays.asList(items));
+    }
+
+
+    /**
+     * Format a collection of objects to a FormattedList. You can access the offsets
+     * of each element from the FormattedList.
+     *
+     * @param items
+     *            items to format. The toString() method is called on each.
+     * @return items formatted into a FormattedList
+     * @draft ICU 67
+     * @provisional This API might change or be removed in a future release.
+     */
+    public FormattedList formatToValue(Collection<?> items) {
+        return formatImpl(items, true).toValue();
     }
 
     // Formats a collection of objects and returns the formatted string plus the offset
     // in the string where the index th element appears. index is zero based. If index is
     // negative or greater than or equal to the size of items then this function returns -1 for
     // the offset.
-    FormattedListBuilder format(Collection<?> items, int index) {
+    FormattedListBuilder formatImpl(Collection<?> items, boolean needsFields) {
         Iterator<?> it = items.iterator();
         int count = items.size();
         switch (count) {
         case 0:
-            return new FormattedListBuilder("", false);
+            return new FormattedListBuilder("", needsFields);
         case 1:
-            return new FormattedListBuilder(it.next(), index == 0);
+            return new FormattedListBuilder(it.next(), needsFields);
         case 2:
-            return new FormattedListBuilder(it.next(), index == 0).append(two, it.next(), index == 1);
+            return new FormattedListBuilder(it.next(), needsFields).append(two, it.next(), 1);
         }
-        FormattedListBuilder builder = new FormattedListBuilder(it.next(), index == 0);
-        builder.append(start, it.next(), index == 1);
+        FormattedListBuilder builder = new FormattedListBuilder(it.next(), needsFields);
+        builder.append(start, it.next(), 1);
         for (int idx = 2; idx < count - 1; ++idx) {
-            builder.append(middle, it.next(), index == idx);
+            builder.append(middle, it.next(), idx);
         }
-        return builder.append(end, it.next(), index == count - 1);
+        return builder.append(end, it.next(), count - 1);
     }
 
     /**
@@ -246,7 +548,7 @@
         if (count <= 0) {
             throw new IllegalArgumentException("count must be > 0");
         }
-        ArrayList<String> list = new ArrayList<String>();
+        ArrayList<String> list = new ArrayList<>();
         for (int i = 0; i < count; i++) {
             list.add(String.format("{%d}", i));
         }
@@ -265,64 +567,74 @@
 
     // Builds a formatted list
     static class FormattedListBuilder {
-        private StringBuilder current;
-        private int offset;
+        private FormattedStringBuilder string;
+        boolean needsFields;
 
-        // Start is the first object in the list; If recordOffset is true, records the offset of
-        // this first object.
-        public FormattedListBuilder(Object start, boolean recordOffset) {
-            this.current = new StringBuilder(start.toString());
-            this.offset = recordOffset ? 0 : -1;
+        // Start is the first object in the list; If needsFields is true, enable the slightly
+        // more expensive code path that records offsets of each element.
+        public FormattedListBuilder(Object start, boolean needsFields) {
+            string = new FormattedStringBuilder();
+            this.needsFields = needsFields;
+            string.setAppendableField(Field.LITERAL);
+            appendElement(start, 0);
         }
 
         // Appends additional object. pattern is a template indicating where the new object gets
         // added in relation to the rest of the list. {0} represents the rest of the list; {1}
-        // represents the new object in pattern. next is the object to be added. If recordOffset
-        // is true, records the offset of next in the formatted string.
-        public FormattedListBuilder append(String pattern, Object next, boolean recordOffset) {
-            int[] offsets = (recordOffset || offsetRecorded()) ? new int[2] : null;
-            SimpleFormatterImpl.formatAndReplace(
-                    pattern, current, offsets, current, next.toString());
-            if (offsets != null) {
-                if (offsets[0] == -1 || offsets[1] == -1) {
-                    throw new IllegalArgumentException(
-                            "{0} or {1} missing from pattern " + pattern);
+        // represents the new object in pattern. next is the object to be added. position is the
+        // index of the next object in the list of inputs.
+        public FormattedListBuilder append(String compiledPattern, Object next, int position) {
+            assert SimpleFormatterImpl.getArgumentLimit(compiledPattern) == 2;
+            string.setAppendIndex(0);
+            long state = 0;
+            while (true) {
+                state = IterInternal.step(state, compiledPattern, string);
+                if (state == IterInternal.DONE) {
+                    break;
                 }
-                if (recordOffset) {
-                    offset = offsets[1];
+                int argIndex = IterInternal.getArgIndex(state);
+                if (argIndex == 0) {
+                    string.setAppendIndex(string.length());
                 } else {
-                    offset += offsets[0];
+                    appendElement(next, position);
                 }
             }
             return this;
         }
 
-        public void appendTo(Appendable appendable) {
-            try {
-                appendable.append(current);
-            } catch(IOException e) {
-                throw new ICUUncheckedIOException(e);
+        private void appendElement(Object element, int position) {
+            if (needsFields) {
+                SpanFieldPlaceholder field = new SpanFieldPlaceholder();
+                field.spanField = SpanField.LIST_SPAN;
+                field.normalField = Field.ELEMENT;
+                field.value = position;
+                string.append(element.toString(), field);
+            } else {
+                string.append(element.toString(), null);
             }
         }
 
+        public void appendTo(Appendable appendable) {
+            Utility.appendTo(string, appendable);
+        }
+
+        public int getOffset(int fieldPositionFoundIndex) {
+            return FormattedValueStringBuilderImpl.findSpan(string, fieldPositionFoundIndex);
+        }
+
         @Override
         public String toString() {
-            return current.toString();
+            return string.toString();
         }
 
-        // Gets the last recorded offset or -1 if no offset recorded.
-        public int getOffset() {
-            return offset;
-        }
-
-        private boolean offsetRecorded() {
-            return offset >= 0;
+        public FormattedList toValue() {
+            return new FormattedList(string);
         }
     }
 
     private static class Cache {
         private final ICUCache<String, ListFormatter> cache =
-            new SimpleCache<String, ListFormatter>();
+            new SimpleCache<>();
 
         public ListFormatter get(ULocale locale, String style) {
             String key = String.format("%s:%s", locale.toString(), style);
@@ -348,4 +660,42 @@
     }
 
     static Cache cache = new Cache();
+
+    static String typeWidthToStyleString(Type type, Width width) {
+        switch (type) {
+            case AND:
+                switch (width) {
+                    case WIDE:
+                        return "standard";
+                    case SHORT:
+                        return "standard-short";
+                    case NARROW:
+                        return "standard-narrow";
+                }
+                break;
+
+            case OR:
+                switch (width) {
+                    case WIDE:
+                        return "or";
+                    case SHORT:
+                        return "or-short";
+                    case NARROW:
+                        return "or-narrow";
+                }
+                break;
+
+            case UNITS:
+                switch (width) {
+                    case WIDE:
+                        return "unit";
+                    case SHORT:
+                        return "unit-short";
+                    case NARROW:
+                        return "unit-narrow";
+                }
+        }
+
+        return null;
+    }
 }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
index 4925c63..fb7799c 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/MeasureFormat.java
@@ -464,7 +464,7 @@
                 results[i] = formatMeasureInteger(measures[i]).toString();
             }
         }
-        FormattedListBuilder builder = listFormatter.format(Arrays.asList(results), -1);
+        FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), false);
         builder.appendTo(appendTo);
     }
 
@@ -811,13 +811,13 @@
             }
             results[i] = result.toString();
         }
-        ListFormatter.FormattedListBuilder builder = listFormatter.format(Arrays.asList(results),
-                fieldPositionFoundIndex);
+        ListFormatter.FormattedListBuilder builder = listFormatter.formatImpl(Arrays.asList(results), true);
 
         // Fix up FieldPosition indexes if our field is found.
-        if (builder.getOffset() != -1) {
-            fieldPosition.setBeginIndex(fpos.getBeginIndex() + builder.getOffset());
-            fieldPosition.setEndIndex(fpos.getEndIndex() + builder.getOffset());
+        int offset = builder.getOffset(fieldPositionFoundIndex);
+        if (offset != -1) {
+            fieldPosition.setBeginIndex(fpos.getBeginIndex() + offset);
+            fieldPosition.setEndIndex(fpos.getEndIndex() + offset);
         }
         builder.appendTo(appendTo);
     }
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java
index 949bace..05ca49f 100644
--- a/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java
+++ b/icu4j/main/classes/core/src/com/ibm/icu/text/RelativeDateTimeFormatter.java
@@ -8,7 +8,6 @@
  */
 package com.ibm.icu.text;
 
-import java.io.IOException;
 import java.io.InvalidObjectException;
 import java.text.AttributedCharacterIterator;
 import java.text.Format;
@@ -24,13 +23,12 @@
 import com.ibm.icu.impl.SoftCache;
 import com.ibm.icu.impl.StandardPlural;
 import com.ibm.icu.impl.UResource;
+import com.ibm.icu.impl.Utility;
 import com.ibm.icu.impl.number.DecimalQuantity;
 import com.ibm.icu.impl.number.DecimalQuantity_DualStorageBCD;
-import com.ibm.icu.impl.number.SimpleModifier;
 import com.ibm.icu.lang.UCharacter;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.ICUException;
-import com.ibm.icu.util.ICUUncheckedIOException;
 import com.ibm.icu.util.ULocale;
 import com.ibm.icu.util.UResourceBundle;
 
@@ -531,13 +529,7 @@
          */
         @Override
         public <A extends Appendable> A appendTo(A appendable) {
-            try {
-                appendable.append(string);
-            } catch (IOException e) {
-                // Throw as an unchecked exception to avoid users needing try/catch
-                throw new ICUUncheckedIOException(e);
-            }
-            return appendable;
+            return Utility.appendTo(string, appendable);
         }
 
         /**
@@ -723,8 +715,7 @@
         StandardPlural pluralForm = StandardPlural.orOtherFromString(pluralKeyword);
 
         String compiledPattern = getRelativeUnitPluralPattern(style, unit, pastFutureIndex, pluralForm);
-        SimpleModifier modifier = new SimpleModifier(compiledPattern, Field.LITERAL, false);
-        modifier.formatAsPrefixSuffix(output, 0, output.length());
+        SimpleFormatterImpl.formatPrefixSuffix(compiledPattern, Field.LITERAL, 0, output.length(), output);
         return output;
     }
 
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java
index a7398a0..095a6d0 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/FormattedStringBuilderTest.java
@@ -8,7 +8,6 @@
 import static org.junit.Assert.assertTrue;
 
 import java.text.FieldPosition;
-import java.text.Format.Field;
 
 import org.junit.Test;
 
@@ -170,7 +169,7 @@
             FormattedStringBuilder sb = new FormattedStringBuilder();
             sb.append(str, null);
             sb.append(str, NumberFormat.Field.CURRENCY);
-            Field[] fields = sb.toFieldArray();
+            Object[] fields = sb.toFieldArray();
             assertEquals(str.length() * 2, fields.length);
             for (int i = 0; i < str.length(); i++) {
                 assertEquals(null, fields[i]);
@@ -198,7 +197,7 @@
             int numNull = 0;
             int numCurr = 0;
             int numInt = 0;
-            Field[] oldFields = fields;
+            Object[] oldFields = fields;
             fields = sb.toFieldArray();
             for (int i = 0; i < sb.length(); i++) {
                 assertEquals(oldFields[i % oldFields.length], fields[i]);
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java
index 2d0367f..8a1d30a 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/ListFormatterTest.java
@@ -9,6 +9,7 @@
 package com.ibm.icu.dev.test.format;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Locale;
 
 import org.junit.Test;
@@ -17,6 +18,9 @@
 
 import com.ibm.icu.dev.test.TestFmwk;
 import com.ibm.icu.text.ListFormatter;
+import com.ibm.icu.text.ListFormatter.FormattedList;
+import com.ibm.icu.text.ListFormatter.Type;
+import com.ibm.icu.text.ListFormatter.Width;
 import com.ibm.icu.util.ULocale;
 
 @RunWith(JUnit4.class)
@@ -209,4 +213,83 @@
         ULocale defaultLocale = ULocale.getDefault(ULocale.Category.FORMAT);
         return defaultLocale.equals(ULocale.ENGLISH) || defaultLocale.equals(ULocale.US);
     }
+
+    @Test
+    public void TestFormattedValue() {
+        ListFormatter fmt = ListFormatter.getInstance(ULocale.ENGLISH);
+
+        {
+            String message = "Field position test 1";
+            String expectedString = "hello, wonderful, and world";
+            String[] inputs = {
+                "hello",
+                "wonderful",
+                "world"
+            };
+            FormattedList result = fmt.formatToValue(Arrays.asList(inputs));
+            Object[][] expectedFieldPositions = new Object[][] {
+                // field, begin index, end index
+                {ListFormatter.SpanField.LIST_SPAN, 0, 5, 0},
+                {ListFormatter.Field.ELEMENT, 0, 5},
+                {ListFormatter.Field.LITERAL, 5, 7},
+                {ListFormatter.SpanField.LIST_SPAN, 7, 16, 1},
+                {ListFormatter.Field.ELEMENT, 7, 16},
+                {ListFormatter.Field.LITERAL, 16, 22},
+                {ListFormatter.SpanField.LIST_SPAN, 22, 27, 2},
+                {ListFormatter.Field.ELEMENT, 22, 27}};
+            FormattedValueTest.checkFormattedValue(
+                message,
+                result,
+                expectedString,
+                expectedFieldPositions);
+        }
+    }
+
+    @Test
+    public void TestCreateStyled() {
+        // Locale en has interesting data
+        Object[][] cases = {
+            { "pt", Type.AND, Width.WIDE, "A, B e C" },
+            { "pt", Type.AND, Width.SHORT, "A, B e C" },
+            { "pt", Type.AND, Width.NARROW, "A, B, C" },
+            { "pt", Type.OR, Width.WIDE, "A, B ou C" },
+            { "pt", Type.OR, Width.SHORT, "A, B ou C" },
+            { "pt", Type.OR, Width.NARROW, "A, B ou C" },
+            { "pt", Type.UNITS, Width.WIDE, "A, B e C" },
+            { "pt", Type.UNITS, Width.SHORT, "A, B e C" },
+            { "pt", Type.UNITS, Width.NARROW, "A B C" },
+            { "en", Type.AND, Width.WIDE, "A, B, and C" },
+            { "en", Type.AND, Width.SHORT, "A, B, & C" },
+            { "en", Type.AND, Width.NARROW, "A, B, C" },
+            { "en", Type.OR, Width.WIDE, "A, B, or C" },
+            { "en", Type.OR, Width.SHORT, "A, B, or C" },
+            { "en", Type.OR, Width.NARROW, "A, B, or C" },
+            { "en", Type.UNITS, Width.WIDE, "A, B, C" },
+            { "en", Type.UNITS, Width.SHORT, "A, B, C" },
+            { "en", Type.UNITS, Width.NARROW, "A B C" },
+        };
+        for (Object[] cas : cases) {
+            Locale loc = new Locale((String) cas[0]);
+            ULocale uloc = new ULocale((String) cas[0]);
+            Type type = (Type) cas[1];
+            Width width = (Width) cas[2];
+            String expected = (String) cas[3];
+            ListFormatter fmt1 = ListFormatter.getInstance(loc, type, width);
+            ListFormatter fmt2 = ListFormatter.getInstance(uloc, type, width);
+            String message = "TestCreateStyled loc="
+                + loc + " type="
+                + type + " width="
+                + width;
+            String[] inputs = {
+                "A",
+                "B",
+                "C"
+            };
+            String result = fmt1.format(Arrays.asList(inputs));
+            assertEquals(message, expected, result);
+            // Coverage for the other factory method overload:
+            result = fmt2.format(Arrays.asList(inputs));
+            assertEquals(message, expected, result);
+        }
+    }
 }
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java
index 8d5eeb4..1ddf239 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/FormatHandler.java
@@ -30,6 +30,7 @@
 import com.ibm.icu.text.DecimalFormat;
 import com.ibm.icu.text.DecimalFormatSymbols;
 import com.ibm.icu.text.DurationFormat;
+import com.ibm.icu.text.ListFormatter;
 import com.ibm.icu.text.MessageFormat;
 import com.ibm.icu.text.NumberFormat;
 import com.ibm.icu.text.PluralFormat;
@@ -1831,6 +1832,36 @@
         }
     }
 
+    public static class ListFormatterFieldHandler implements SerializableTestUtility.Handler
+    {
+        @Override
+        public Object[] getTestObjects()
+        {
+            return new Object[] {ListFormatter.Field.ELEMENT, ListFormatter.Field.LITERAL};
+        }
+
+        @Override
+        public boolean hasSameBehavior(Object a, Object b)
+        {
+            return (a == b);
+        }
+    }
+
+    public static class ListFormatterSpanFieldHandler implements SerializableTestUtility.Handler
+    {
+        @Override
+        public Object[] getTestObjects()
+        {
+            return new Object[] {ListFormatter.SpanField.LIST_SPAN};
+        }
+
+        @Override
+        public boolean hasSameBehavior(Object a, Object b)
+        {
+            return (a == b);
+        }
+    }
+
     public static class DateFormatHandler implements SerializableTestUtility.Handler
     {
         static HashMap cannedPatterns = new HashMap();
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java
index 7dba946..5d4ef83 100644
--- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTestUtility.java
@@ -820,6 +820,8 @@
         map.put("com.ibm.icu.text.MessageFormat$Field", new FormatHandler.MessageFormatFieldHandler());
         map.put("com.ibm.icu.text.RelativeDateTimeFormatter$Field", new FormatHandler.RelativeDateTimeFormatterFieldHandler());
         map.put("com.ibm.icu.text.DateIntervalFormat$SpanField", new FormatHandler.DateIntervalSpanFieldHandler());
+        map.put("com.ibm.icu.text.ListFormatter$Field", new FormatHandler.ListFormatterFieldHandler());
+        map.put("com.ibm.icu.text.ListFormatter$SpanField", new FormatHandler.ListFormatterSpanFieldHandler());
 
         map.put("com.ibm.icu.impl.duration.BasicDurationFormat", new FormatHandler.BasicDurationFormatHandler());
         map.put("com.ibm.icu.impl.RelativeDateFormat", new FormatHandler.RelativeDateFormatHandler());