вторник, 13 декабря 2011 г.

Исходники

Исходники ко всем статьям перенесены на Google Code. Линки к статьям: 3D Carousel демо:
3D Carousel

ImageView с поддержкой SVG:
SVG ImageView

Остальные статьи объединены в один проект:
Android.Tips

Линки в статьях также обновлены.

вторник, 6 декабря 2011 г.

Использование маски с TextEdit



Source Code
English translation

В продолжение темы о фоорматировании текста при помощи регулярных врыжений мы
разработаем функциональность для наложения маски на EditText.

Конечно, мы можем воспользоваться InputType, но хотелось бы иметь более гибкую функцтональность.


Для начала посмотрим на класс MaskFormatter в Swing.
Он делает все, что нам надо. Изменим кое-что в этом классе.
Оставим внутренние классы как есть. Но MaskFormatter работает
с JFormattedTextField. Необходимо удалить весь функционал, связанный с этим контролом.
В результате, мы имеем что-то типа этого:
public class MaskedFormatter {
 
    // Potential values in mask.
    private static final char DIGIT_KEY = '#';
    private static final char LITERAL_KEY = '\'';
    private static final char UPPERCASE_KEY = 'U';
    private static final char LOWERCASE_KEY = 'L';
    private static final char ALPHA_NUMERIC_KEY = 'A';
    private static final char CHARACTER_KEY = '?';
    private static final char ANYTHING_KEY = '*';
    private static final char HEX_KEY = 'H';
    
    /** The user specified mask. */
    private String mask;    
    
    /** Indicates if the value contains the literal characters. */
    private boolean containsLiteralChars;
    
    private static final MaskCharacter[] EmptyMaskChars = 
           new MaskCharacter[0];

    /** List of valid characters. */
    private String validCharacters;
    
    /** List of invalid characters. */
    private String invalidCharacters;

    /** String used to represent characters not present. */
    private char placeholder;  
    
    /** String used for the passed in value if it does not completely
     * fill the mask. */
    private String placeholderString;    
    
    private transient MaskCharacter[] maskChars;
    
    
    /** Indicates if the value being edited must match the mask. */
    @SuppressWarnings("unused")
 private boolean allowsInvalid;
    
    
    /**
     * Creates a MaskFormatter with no mask.
     */
    public MaskedFormatter() {
        setAllowsInvalid(false);
        containsLiteralChars = true;
        maskChars = EmptyMaskChars;
        placeholder = ' ';
    }

    /**
     * Creates a MaskFormatter with the specified mask.
     * A ParseException
     * will be thrown if mask is an invalid mask.
     *
     * @throws ParseException if mask does not contain valid mask characters
     */
    public MaskedFormatter(String mask) throws ParseException {
        this();
        setMask(mask);
    }    
    
    /**
     * Sets the mask dictating the legal characters.
     * This will throw a ParseException if mask is
     * not valid.
     *
     * @throws ParseException if mask does not contain valid mask characters
     */
    public void setMask(String mask) throws ParseException {
        this.mask = mask;
        updateInternalMask();
    }
    
    /**
     * Returns the formatting mask.
     *
     * @return Mask dictating legal character values.
     */
    public String getMask() {
        return mask;
    }    
    
    /**
     * Updates the internal representation of the mask.
     */
    private void updateInternalMask() throws ParseException {
        String mask = getMask();
        ArrayList<MaskCharacter> fixed = new ArrayList<MaskCharacter>();
        ArrayList<MaskCharacter> temp = fixed;

        if (mask != null) {
            for (int counter = 0, maxCounter = mask.length();
                 counter < maxCounter; counter++) {
                char maskChar = mask.charAt(counter);

                switch (maskChar) {
                case DIGIT_KEY:
                    temp.add(new DigitMaskCharacter());
                    break;
                case LITERAL_KEY:
                    if (++counter < maxCounter) {
                        maskChar = mask.charAt(counter);
                        temp.add(new LiteralCharacter(maskChar));
                    }
                    // else: Could actually throw if else
                    break;
                case UPPERCASE_KEY:
                    temp.add(new UpperCaseCharacter());
                    break;
                case LOWERCASE_KEY:
                    temp.add(new LowerCaseCharacter());
                    break;
                case ALPHA_NUMERIC_KEY:
                    temp.add(new AlphaNumericCharacter());
                    break;
                case CHARACTER_KEY:
                    temp.add(new CharCharacter());
                    break;
                case ANYTHING_KEY:
                    temp.add(new MaskCharacter());
                    break;
                case HEX_KEY:
                    temp.add(new HexCharacter());
                    break;
                default:
                    temp.add(new LiteralCharacter(maskChar));
                    break;
                }
            }
        }
        if (fixed.size() == 0) {
            maskChars = EmptyMaskChars;
        }
        else {
            maskChars = new MaskCharacter[fixed.size()];
            fixed.toArray(maskChars);
        }
    }    
    
    
    /**
     * Sets whether or not the value being edited is allowed to be invalid
     * for a length of time (that is, stringToValue throws
     * a ParseException).
     * It is often convenient to allow the user to temporarily input an
     * invalid value.
     *
     * @param allowsInvalid Used to indicate if the edited value must always
     *        be valid
     */
    public void setAllowsInvalid(boolean allowsInvalid) {
        this.allowsInvalid = allowsInvalid;
    }    


    /**
     * Allows for further restricting of the characters that can be input.
     * Only characters specified in the mask, not in the
     * invalidCharacters, and in
     * validCharacters will be allowed to be input. Passing
     * in null (the default) implies the valid characters are only bound
     * by the mask and the invalid characters.
     *
     * @param validCharacters If non-null, specifies legal characters.
     */
    public void setValidCharacters(String validCharacters) {
        this.validCharacters = validCharacters;
    }

    /**
     * Returns the valid characters that can be input.
     *
     * @return Legal characters
     */
    public String getValidCharacters() {
        return validCharacters;
    }
 
    /**
     * Allows for further restricting of the characters that can be input.
     * Only characters specified in the mask, not in the
     * invalidCharacters, and in
     * validCharacters will be allowed to be input. Passing
     * in null (the default) implies the valid characters are only bound
     * by the mask and the valid characters.
     *
     * @param invalidCharacters If non-null, specifies illegal characters.
     */
    public void setInvalidCharacters(String invalidCharacters) {
        this.invalidCharacters = invalidCharacters;
    }

    /**
     * Returns the characters that are not valid for input.
     *
     * @return illegal characters.
     */
    public String getInvalidCharacters() {
        return invalidCharacters;
    }    
    
    /**
     * If true, the returned value and set value will also contain the literal
     * characters in mask.
     * 
     * For example, if the mask is '(###) ###-####', the
     * current value is '(415) 555-1212', and
     * valueContainsLiteralCharacters is
     * true stringToValue will return
     * '(415) 555-1212'. On the other hand, if
     * valueContainsLiteralCharacters is false,
     * stringToValue will return '4155551212'.
     *
     * @param containsLiteralChars Used to indicate if literal characters in
     *        mask should be returned in stringToValue
     */
    public void setValueContainsLiteralCharacters(
                        boolean containsLiteralChars) {
        this.containsLiteralChars = containsLiteralChars;
    }

    /**
     * Returns true if stringToValue should return literal
     * characters in the mask.
     *
     * @return True if literal characters in mask should be returned in
     *         stringToValue
     */
    public boolean getValueContainsLiteralCharacters() {
        return containsLiteralChars;
    }    
    
    /**
     * Sets the character to use in place of characters that are not present
     * in the value, ie the user must fill them in. The default value is
     * a space.
     * 
     * This is only applicable if the placeholder string has not been
     * specified, or does not completely fill in the mask.
     *
     * @param placeholder Character used when formatting if the value does not
     *        completely fill the mask
     */
    public void setPlaceholderCharacter(char placeholder) {
        this.placeholder = placeholder;
    }

    /**
     * Returns the character to use in place of characters that are not present
     * in the value, ie the user must fill them in.
     *
     * @return Character used when formatting if the value does not
     *        completely fill the mask
     */
    public char getPlaceholderCharacter() {
        return placeholder;
    }
    
    /**
     * Sets the string to use if the value does not completely fill in
     * the mask. A null value implies the placeholder char should be used.
     *
     * @param placeholder String used when formatting if the value does not
     *        completely fill the mask
     */
    public void setPlaceholder(String placeholder) {
        this.placeholderString = placeholder;
    }

    /**
     * Returns the String to use if the value does not completely fill
     * in the mask.
     *
     * @return String used when formatting if the value does not
     *        completely fill the mask
     */
    public String getPlaceholder() {
        return placeholderString;
    }    
    /**
     * Returns a String representation of the Object value
     * based on the mask.  Refer to
     * {@link #setValueContainsLiteralCharacters} for details
     * on how literals are treated.
     *
     * @throws ParseException if there is an error in the conversion
     * @param value Value to convert
     * @see #setValueContainsLiteralCharacters
     * @return String representation of value
     */
    public String valueToString(Object value) throws ParseException {
        String sValue = (value == null) ? "" : value.toString();
        StringBuilder result = new StringBuilder();
        String placeholder = getPlaceholder();
        int[] valueCounter = { 0 };

        append(result, sValue, valueCounter, placeholder, maskChars);
        return result.toString();
    }    
    
    /**
     * Invokes append on the mask characters in
     * mask.
     */
    private void append(StringBuilder result, String value, int[] index,
                        String placeholder, MaskCharacter[] mask)
                          throws ParseException {
        for (int counter = 0, maxCounter = mask.length;
             counter < maxCounter; counter++) {
            mask[counter].append(result, value, index, placeholder);
        }
    }    



И чтобы упростить жизнь создадим класс TextWatcher для работы с форматтером:
 public class MaskedWatcher implements TextWatcher {
 
 private String mMask;
 String mResult = ""; 
 
 public MaskedWatcher(String mask){
  mMask = mask;
 }

 @Override
 public void afterTextChanged(Editable s) {
  
  String mask = mMask;
  String value = s.toString();
  
  if(value.equals(mResult))
   return;

  try {
   
   // prepare the formatter
   MaskedFormatter formatter = new MaskedFormatter(mask);
   formatter.setValueContainsLiteralCharacters(false);
   formatter.setPlaceholderCharacter((char)1);
   
   // get a string with applied mask and placeholder chars
   value = formatter.valueToString(value);
   
   try{
    
    // find first placeholder
    value = value.substring(0, value.indexOf((char)1));

    //process a mask char
    if(value.charAt(value.length()-1) == 
                                      mask.charAt(value.length()-1)){
     value = value.substring(0, value.length() - 1);
    }
    
   }
   catch(Exception e){}
   
   mResult = value;
   
   s.replace(0, s.length(), value);
   
   
  } catch (ParseException e) {
   
   //the entered value does not match a mask
   int offset = e.getErrorOffset();
   value = removeCharAt(value, offset);
   s.replace(0, s.length(), value);
   
  }
  
  
 }

 @Override
 public void beforeTextChanged(CharSequence s, int start, int count,
   int after) {
 }

 @Override
 public void onTextChanged(CharSequence s, int start, 
             int before, int count) {
 }

 public static String removeCharAt(String s, int pos) {

  StringBuffer buffer = new StringBuffer(s.length() - 1);
  buffer.append(s.substring(0, pos)).append(s.substring(pos + 1));
  return buffer.toString();

 } 
 
}


Теперь мы можем работать с масками:
        EditText phone = (EditText)findViewById(R.id.phone);
        phone.addTextChangedListener(
          new MaskedWatcher("(###) ###-##-##")
        )