Converts the selected item from the drop down list into a sequence
+ * of character that can be used in the edit box.
+ *
+ * @param selectedItem the item selected by the user for completion
+ *
+ * @return a sequence of characters representing the selected suggestion
+ */
+ protected CharSequence convertSelectionToString(Object selectedItem) {
+ switch (mAutoCompleteMode){
+ case AUTOCOMPLETE_MODE_SINGLE:
+ return ((InternalAutoCompleteTextView)mInputView).superConvertSelectionToString(selectedItem);
+ case AUTOCOMPLETE_MODE_MULTI:
+ return ((InternalMultiAutoCompleteTextView)mInputView).superConvertSelectionToString(selectedItem);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Starts filtering the content of the drop down list. The filtering
+ * pattern is the content of the edit box. Subclasses should override this
+ * method to filter with a different pattern, for instance a substring of
+ * text
.
+ *
+ * @param text the filtering pattern
+ * @param keyCode the last character inserted in the edit box; beware that
+ * this will be null when text is being added through a soft input method.
+ */
+ protected void performFiltering(CharSequence text, int keyCode) {
+ switch (mAutoCompleteMode){
+ case AUTOCOMPLETE_MODE_SINGLE:
+ ((InternalAutoCompleteTextView)mInputView).superPerformFiltering(text, keyCode);
+ break;
+ case AUTOCOMPLETE_MODE_MULTI:
+ ((InternalMultiAutoCompleteTextView)mInputView).superPerformFiltering(text, keyCode);
+ break;
+ }
+ }
+
+ /**
+ * Performs the text completion by replacing the current text by the
+ * selected item. Subclasses should override this method to avoid replacing
+ * the whole content of the edit box.
+ *
+ * @param text the selected suggestion in the drop down list
+ */
+ protected void replaceText(CharSequence text) {
+ switch (mAutoCompleteMode){
+ case AUTOCOMPLETE_MODE_SINGLE:
+ ((InternalAutoCompleteTextView)mInputView).superReplaceText(text);
+ break;
+ case AUTOCOMPLETE_MODE_MULTI:
+ ((InternalMultiAutoCompleteTextView)mInputView).superReplaceText(text);
+ break;
+ }
+ }
+
+ /**
+ * Returns the Filter obtained from {@link Filterable#getFilter},
+ * or Starts filtering the content of the drop down list. The filtering
+ * pattern is the specified range of text from the edit box. Subclasses may
+ * override this method to filter with a different pattern, for
+ * instance a smaller substring of text
.
+ */
+ protected void performFiltering(CharSequence text, int start, int end, int keyCode) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_MULTI)
+ ((InternalMultiAutoCompleteTextView)mInputView).superPerformFiltering(text, start, end, keyCode);
+ }
+
+ /* public method of AutoCompleteTextView */
+
+ /**
+ * Sets the optional hint text that is displayed at the bottom of the
+ * the matching list. This can be used as a cue to the user on how to
+ * best use the list, or to provide extra information.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param hint the text to be displayed to the user
+ *
+ * @see #getCompletionHint()
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ */
+ public void setCompletionHint(CharSequence hint) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setCompletionHint(hint);
+ }
+
+ /**
+ * Gets the optional hint text displayed at the bottom of the the matching list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return The hint text, if any
+ *
+ * @see #setCompletionHint(CharSequence)
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public CharSequence getCompletionHint() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
+ return null;
+ return ((AutoCompleteTextView)mInputView).getCompletionHint();
+ }
+
+ /**
+ * Returns the current width for the auto-complete drop down list. This can
+ * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the width for the drop down list
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+ */
+ public int getDropDownWidth() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getDropDownWidth();
+ }
+
+ /**
+ * Sets the current width for the auto-complete drop down list. This can
+ * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param width the width to use
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+ */
+ public void setDropDownWidth(int width) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownWidth(width);
+ }
+
+ /**
+ * Returns the current height for the auto-complete drop down list. This can
+ * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
+ * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
+ * of the drop down's content.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the height for the drop down list
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+ */
+ public int getDropDownHeight() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getDropDownHeight();
+ }
+
+ /**
+ * Sets the current height for the auto-complete drop down list. This can
+ * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
+ * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
+ * of the drop down's content.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param height the height to use
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+ */
+ public void setDropDownHeight(int height) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownHeight(height);
+ }
+
+ /**
+ * Returns the id for the view that the auto-complete drop down list is anchored to.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the view's id, or {@link View#NO_ID} if none specified
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+ */
+ public int getDropDownAnchor() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getDropDownAnchor();
+ }
+
+ /**
+ * Sets the view to which the auto-complete drop down list should anchor. The view
+ * corresponding to this id will not be loaded until the next time it is needed to avoid
+ * loading a view which is not yet instantiated.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param id the id to anchor the drop down list view to
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+ */
+ public void setDropDownAnchor(int id) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownAnchor(id);
+ }
+
+ /**
+ * Gets the background of the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the background drawable
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public Drawable getDropDownBackground() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return null;
+ return ((AutoCompleteTextView)mInputView).getDropDownBackground();
+ }
+
+ /**
+ * Sets the background of the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param d the drawable to set as the background
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public void setDropDownBackgroundDrawable(Drawable d) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownBackgroundDrawable(d);
+ }
+
+ /**
+ * Sets the background of the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param id the id of the drawable to set as the background
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ */
+ public void setDropDownBackgroundResource(int id) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownBackgroundResource(id);
+ }
+
+ /**
+ * Sets the vertical offset used for the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param offset the vertical offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public void setDropDownVerticalOffset(int offset) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownVerticalOffset(offset);
+ }
+
+ /**
+ * Gets the vertical offset used for the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the vertical offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public int getDropDownVerticalOffset() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getDropDownVerticalOffset();
+ }
+
+ /**
+ * Sets the horizontal offset used for the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param offset the horizontal offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public void setDropDownHorizontalOffset(int offset) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setDropDownHorizontalOffset(offset);
+ }
+
+ /**
+ * Gets the horizontal offset used for the auto-complete drop-down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the horizontal offset
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public int getDropDownHorizontalOffset() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getDropDownHorizontalOffset();
+ }
+
+ /**
+ * Returns the number of characters the user must type before the drop
+ * down list is shown.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the minimum number of characters to type to show the drop down
+ *
+ * @see #setThreshold(int)
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+ */
+ public int getThreshold() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getThreshold();
+ }
+
+ /**
+ * Specifies the minimum number of characters the user has to type in the
+ * edit box before the drop down list is shown.
+ *
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param threshold the number of characters to type before the drop down
+ * is shown
+ *
+ * @see #getThreshold()
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+ */
+ public void setThreshold(int threshold) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setThreshold(threshold);
+ }
+
+ /**
+ * Sets the listener that will be notified when the user clicks an item
+ * in the drop down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param l the item click listener
+ */
+ public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setOnItemClickListener(l);
+ }
+
+ /**
+ * Sets the listener that will be notified when the user selects an item
+ * in the drop down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param l the item selected listener
+ */
+ public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setOnItemSelectedListener(l);
+ }
+
+ /**
+ * Returns the listener that is notified whenever the user clicks an item
+ * in the drop down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the item click listener
+ */
+ public AdapterView.OnItemClickListener getOnItemClickListener() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return null;
+ return ((AutoCompleteTextView)mInputView).getOnItemClickListener();
+ }
+
+ /**
+ * Returns the listener that is notified whenever the user selects an
+ * item in the drop down list.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the item selected listener
+ */
+ public AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return null;
+ return ((AutoCompleteTextView)mInputView).getOnItemSelectedListener();
+ }
+
+ /**
+ * Returns a filterable list adapter used for auto completion.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return a data adapter used for auto completion
+ */
+ public ListAdapter getAdapter() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return null;
+ return ((AutoCompleteTextView)mInputView).getAdapter();
+ }
+
+ /**
+ * Changes the list of data used for auto completion. The provided list
+ * must be a filterable list adapter.
+ *
+ * The caller is still responsible for managing any resources used by the adapter.
+ * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
+ * A common case is the use of {@link CursorAdapter}, which
+ * contains a {@link android.database.Cursor} that must be closed. This can be done
+ * automatically (see
+ * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
+ * startManagingCursor()}),
+ * or by manually closing the cursor when the AutoCompleteTextView is dismissed.
+ *
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param adapter the adapter holding the auto completion data
+ *
+ * @see #getAdapter()
+ * @see Filterable
+ * @see ListAdapter
+ */
+ public void setAdapter(T adapter) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setAdapter(adapter);
+ }
+
+ /**
+ * Returns true
if the amount of text in the field meets
+ * or exceeds the {@link #getThreshold} requirement. You can override
+ * this to impose a different standard for when filtering will be
+ * triggered.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ */
+ public boolean enoughToFilter() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return false;
+ return ((AutoCompleteTextView)mInputView).enoughToFilter();
+ }
+
+ /**
+ * Indicates whether the popup menu is showing.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return true if the popup menu is showing, false otherwise
+ */
+ public boolean isPopupShowing() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return false;
+ return ((AutoCompleteTextView)mInputView).isPopupShowing();
+ }
+
+ /**
+ * Clear the list selection. This may only be temporary, as user input will often bring
+ * it back.
+ *
Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ */
+ public void clearListSelection() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).clearListSelection();
+ }
+
+ /**
+ * Set the position of the dropdown view selection.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param position The position to move the selector to.
+ */
+ public void setListSelection(int position) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setListSelection(position);
+ }
+
+ /**
+ * Get the position of the dropdown view selection, if there is one. Returns
+ * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
+ * there is no selection.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @return the position of the current selection, if there is one, or
+ * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
+ *
+ * @see ListView#getSelectedItemPosition()
+ */
+ public int getListSelection() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return 0;
+ return ((AutoCompleteTextView)mInputView).getListSelection();
+ }
+
+ /**
+ * Performs the text completion by converting the selected item from
+ * the drop down list into a string, replacing the text box's content with
+ * this string and finally dismissing the drop down menu.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ */
+ public void performCompletion() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).performCompletion();
+ }
+
+ /**
+ * Identifies whether the view is currently performing a text completion, so subclasses
+ * can decide whether to respond to text changed events.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ */
+ public boolean isPerformingCompletion() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return false;
+ return ((AutoCompleteTextView)mInputView).isPerformingCompletion();
+ }
+
+ /** Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
*/
+ public void onFilterComplete(int count) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ ((InternalAutoCompleteTextView)mInputView).superOnFilterComplete(count);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_MULTI)
+ ((InternalMultiAutoCompleteTextView)mInputView).superOnFilterComplete(count);
+ }
+
+ /**
+ * Closes the drop down if present on screen.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ */
+ public void dismissDropDown() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).dismissDropDown();
+ }
+
+ /**
+ * Displays the drop down on screen.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ */
+ public void showDropDown() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).showDropDown();
+ }
+
+ /**
+ * Sets the validator used to perform text validation.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @param validator The validator used to validate the text entered in this widget.
+ *
+ * @see #getValidator()
+ * @see #performValidation()
+ */
+ public void setValidator(AutoCompleteTextView.Validator validator) {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).setValidator(validator);
+ }
+
+ /**
+ * Returns the Validator set with {@link #setValidator},
+ * or null
if it was not set.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @see #setValidator(AutoCompleteTextView.Validator)
+ * @see #performValidation()
+ */
+ public AutoCompleteTextView.Validator getValidator() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return null;
+ return ((AutoCompleteTextView)mInputView).getValidator();
+ }
+
+ /**
+ * If a validator was set on this view and the current string is not valid,
+ * ask the validator to fix it.
+ * Only work when autoComplete mode is {@link #AUTOCOMPLETE_MODE_SINGLE} or {@link #AUTOCOMPLETE_MODE_MULTI}
+ *
+ * @see #getValidator()
+ * @see #setValidator(AutoCompleteTextView.Validator)
+ */
+ public void performValidation() {
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return;
+ ((AutoCompleteTextView)mInputView).performValidation();
+ }
+
+ /**
+ * Sets the Tokenizer that will be used to determine the relevant
+ * range of the text where the user is typing.
+ * Only work when autoCompleMode is AUTOCOMPLETE_MODE_MULTI
+ */
+ public void setTokenizer(MultiAutoCompleteTextView.Tokenizer t) {
+ if(mAutoCompleteMode != AUTOCOMPLETE_MODE_MULTI)
+ return;
+ ((MultiAutoCompleteTextView)mInputView).setTokenizer(t);
+ }
+
+ /* public method of EditText */
+
+ @Override
+ public void setEnabled(boolean enabled){
+ mInputView.setEnabled(enabled);
+ }
+
+ /**
+ * Convenience for {@link android.text.Selection#extendSelection}.
+ */
+ public void extendSelection (int index){
+ mInputView.extendSelection(index);
+ }
+
+ public Editable getText (){
+ return mInputView.getText();
+ }
+
+ /**
+ * Convenience for {@link android.text.Selection#selectAll}.
+ */
+ public void selectAll (){
+ mInputView.selectAll();
+ }
+
+ /**
+ * Causes words in the text that are longer than the view is wide
+ * to be ellipsized instead of broken in the middle. You may also
+ * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
+ * to constrain the text to a single line. Use null
+ * to turn off ellipsizing.
+ *
+ * If {@link #setMaxLines} has been used to set two or more lines,
+ * only {@link TruncateAt#END} and
+ * {@link TruncateAt#MARQUEE} are supported
+ * (other ellipsizing types will not do anything).
+ *
+ * @attr ref android.R.styleable#TextView_ellipsize
+ */
+ public void setEllipsize (TruncateAt ellipsis){
+ mInputView.setEllipsize(ellipsis);
+ }
+
+ /**
+ * Convenience for {@link android.text.Selection#setSelection(Spannable, int)}.
+ */
+ public void setSelection (int index){
+ mInputView.setSelection(index);
+ }
+
+ /**
+ * Convenience for {@link android.text.Selection#setSelection(Spannable, int, int)}.
+ */
+ public void setSelection (int start, int stop){
+ mInputView.setSelection(start, stop);
+ }
+
+ public void setText (CharSequence text, TextView.BufferType type){
+ mInputView.setText(text, type);
+ }
+
+ /**
+ * Adds a TextWatcher to the list of those whose methods are called
+ * whenever this TextView's text changes.
+ *
+ * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
+ * not called after {@link #setText} calls. Now, doing {@link #setText}
+ * if there are any text changed listeners forces the buffer type to
+ * Editable if it would not otherwise be and does call this method.
+ */
+ public void addTextChangedListener(TextWatcher textWatcher){
+ mInputView.addTextChangedListener(textWatcher);
+ }
+
+ /**
+ * Convenience method: Append the specified text to the TextView's
+ * display buffer, upgrading it to BufferType.EDITABLE if it was
+ * not already editable.
+ */
+ public final void append (CharSequence text){
+ mInputView.append(text);
+ }
+
+ /**
+ * Convenience method: Append the specified text slice to the TextView's
+ * display buffer, upgrading it to BufferType.EDITABLE if it was
+ * not already editable.
+ */
+ public void append (CharSequence text, int start, int end){
+ mInputView.append(text, start, end);
+ }
+
+ public void beginBatchEdit (){
+ mInputView.beginBatchEdit();
+ }
+
+ /**
+ * Move the point, specified by the offset, into the view if it is needed.
+ * This has to be called after layout. Returns true if anything changed.
+ */
+ public boolean bringPointIntoView (int offset){
+ return mInputView.bringPointIntoView(offset);
+ }
+
+ public void cancelLongPress (){
+ mInputView.cancelLongPress();
+ }
+
+ /**
+ * Use {@link android.view.inputmethod.BaseInputConnection#removeComposingSpans
+ * BaseInputConnection.removeComposingSpans()} to remove any IME composing
+ * state from this text view.
+ */
+ public void clearComposingText (){
+ mInputView.clearComposingText();
+ }
+
+ @Override
+ public void computeScroll (){
+ mInputView.computeScroll();
+ }
+
+ @Override
+ public void debug (int depth){
+ mInputView.debug(depth);
+ }
+
+ /**
+ * Returns true, only while processing a touch gesture, if the initial
+ * touch down event caused focus to move to the text view and as a result
+ * its selection changed. Only valid while processing the touch gesture
+ * of interest, in an editable text view.
+ */
+ public boolean didTouchFocusSelect (){
+ return mInputView.didTouchFocusSelect();
+ }
+
+ public void endBatchEdit (){
+ mInputView.endBatchEdit();
+ }
+
+ /**
+ * If this TextView contains editable content, extract a portion of it
+ * based on the information in request in to outText.
+ * @return Returns true if the text was successfully extracted, else false.
+ */
+ public boolean extractText (ExtractedTextRequest request, ExtractedText outText){
+ return mInputView.extractText(request, outText);
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public void findViewsWithText (ArrayList outViews, CharSequence searched, int flags){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ mInputView.findViewsWithText(outViews, searched, flags);
+ }
+
+ /**
+ * Gets the autolink mask of the text. See {@link
+ * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+ * possible values.
+ *
+ * @attr ref android.R.styleable#TextView_autoLink
+ */
+ public final int getAutoLinkMask (){
+ return mInputView.getAutoLinkMask();
+ }
+
+ @Override
+ public int getBaseline (){
+ return mInputView.getBaseline();
+ }
+
+ /**
+ * Returns the padding between the compound drawables and the text.
+ *
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ */
+ public int getCompoundDrawablePadding (){
+ return mInputView.getCompoundDrawablePadding();
+ }
+
+ /**
+ * Returns drawables for the left, top, right, and bottom borders.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public Drawable[] getCompoundDrawables (){
+ return mInputView.getCompoundDrawables();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public Drawable[] getCompoundDrawablesRelative (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ return mInputView.getCompoundDrawablesRelative();
+
+ return mInputView.getCompoundDrawables();
+ }
+
+ /**
+ * Returns the bottom padding of the view, plus space for the bottom
+ * Drawable if any.
+ */
+ public int getCompoundPaddingBottom (){
+ return mInputView.getCompoundPaddingBottom();
+ }
+
+ /**
+ * Returns the end padding of the view, plus space for the end
+ * Drawable if any.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public int getCompoundPaddingEnd (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ return mInputView.getCompoundPaddingEnd();
+
+ return mInputView.getCompoundPaddingRight();
+ }
+
+ /**
+ * Returns the left padding of the view, plus space for the left
+ * Drawable if any.
+ */
+ public int getCompoundPaddingLeft (){
+ return mInputView.getCompoundPaddingLeft();
+ }
+
+ /**
+ * Returns the right padding of the view, plus space for the right
+ * Drawable if any.
+ */
+ public int getCompoundPaddingRight (){
+ return mInputView.getCompoundPaddingRight();
+ }
+
+ /**
+ * Returns the start padding of the view, plus space for the start
+ * Drawable if any.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public int getCompoundPaddingStart (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ return mInputView.getCompoundPaddingStart();
+
+ return mInputView.getCompoundPaddingLeft();
+ }
+
+ /**
+ * Returns the top padding of the view, plus space for the top
+ * Drawable if any.
+ */
+ public int getCompoundPaddingTop (){
+ return mInputView.getCompoundPaddingTop();
+ }
+
+ /**
+ * Return the current color selected to paint the hint text.
+ *
+ * @return Returns the current hint text color.
+ */
+ public final int getCurrentHintTextColor (){
+ return mInputView.getCurrentHintTextColor();
+ }
+
+ /**
+ * Return the current color selected for normal text.
+ *
+ * @return Returns the current text color.
+ */
+ public final int getCurrentTextColor (){
+ return mInputView.getCurrentTextColor();
+ }
+
+ /**
+ * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
+ *
+ * @return The current custom selection callback.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public ActionMode.Callback getCustomSelectionActionModeCallback (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ return mInputView.getCustomSelectionActionModeCallback();
+
+ return null;
+ }
+
+ /**
+ * Return the text the TextView is displaying as an Editable object. If
+ * the text is not editable, null is returned.
+ *
+ * @see #getText
+ */
+ public Editable getEditableText (){
+ return mInputView.getEditableText();
+ }
+
+ /**
+ * Returns where, if anywhere, words that are longer than the view
+ * is wide should be ellipsized.
+ */
+ public TruncateAt getEllipsize (){
+ return mInputView.getEllipsize();
+ }
+
+ /**
+ * Returns the extended bottom padding of the view, including both the
+ * bottom Drawable if any and any extra space to keep more than maxLines
+ * of text from showing. It is only valid to call this after measuring.
+ */
+ public int getExtendedPaddingBottom (){
+ return mInputView.getExtendedPaddingBottom();
+ }
+
+ /**
+ * Returns the extended top padding of the view, including both the
+ * top Drawable if any and any extra space to keep more than maxLines
+ * of text from showing. It is only valid to call this after measuring.
+ */
+ public int getExtendedPaddingTop (){
+ return mInputView.getExtendedPaddingTop();
+ }
+
+ /**
+ * Returns the current list of input filters.
+ *
+ * @attr ref android.R.styleable#TextView_maxLength
+ */
+ public InputFilter[] getFilters (){
+ return mInputView.getFilters();
+ }
+
+ @Override
+ public void getFocusedRect (@NonNull Rect r){
+ mInputView.getFocusedRect(r);
+ }
+
+ /**
+ * @return the currently set font feature settings. Default is null.
+ *
+ * @see #setFontFeatureSettings(String)
+ * @see android.graphics.Paint#setFontFeatureSettings
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public String getFontFeatureSettings (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ return mInputView.getFontFeatureSettings();
+ return null;
+ }
+
+ /**
+ * Return whether this text view is including its entire text contents
+ * in frozen icicles.
+ *
+ * @return Returns true if text is included, false if it isn't.
+ *
+ * @see #setFreezesText
+ */
+ public boolean getFreezesText (){
+ return mInputView.getFreezesText();
+ }
+
+ /**
+ * Returns the horizontal and vertical alignment of this TextView.
+ *
+ * @see Gravity
+ * @attr ref android.R.styleable#TextView_gravity
+ */
+ public int getGravity (){
+ return mInputView.getGravity();
+ }
+
+ /**
+ * @return the color used to display the selection highlight
+ *
+ * @see #setHighlightColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getHighlightColor (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getHighlightColor();
+
+ return 0;
+ }
+
+ /**
+ * Returns the hint that is displayed when the text of the TextView
+ * is empty.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ public CharSequence getHint (){
+ return mInputView.getHint();
+ }
+
+ /**
+ * @return the color of the hint text, for the different states of this TextView.
+ *
+ * @see #setHintTextColor(ColorStateList)
+ * @see #setHintTextColor(int)
+ * @see #setTextColor(ColorStateList)
+ * @see #setLinkTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final ColorStateList getHintTextColors (){
+ return mInputView.getHintTextColors();
+ }
+
+ /**
+ * Get the IME action ID previous set with {@link #setImeActionLabel}.
+ *
+ * @see #setImeActionLabel
+ * @see EditorInfo
+ */
+ public int getImeActionId (){
+ return mInputView.getImeActionId();
+ }
+
+ /**
+ * Get the IME action label previous set with {@link #setImeActionLabel}.
+ *
+ * @see #setImeActionLabel
+ * @see EditorInfo
+ */
+ public CharSequence getImeActionLabel (){
+ return mInputView.getImeActionLabel();
+ }
+
+ /**
+ * Get the type of the IME editor.
+ *
+ * @see #setImeOptions(int)
+ * @see EditorInfo
+ */
+ public int getImeOptions (){
+ return mInputView.getImeOptions();
+ }
+
+ /**
+ * Gets whether the TextView includes extra top and bottom padding to make
+ * room for accents that go above the normal ascent and descent.
+ *
+ * @see #setIncludeFontPadding(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean getIncludeFontPadding (){
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mInputView.getIncludeFontPadding();
+ }
+
+ /**
+ * Retrieve the input extras currently associated with the text view, which
+ * can be viewed as well as modified.
+ *
+ * @param create If true, the extras will be created if they don't already
+ * exist. Otherwise, null will be returned if none have been created.
+ * @see #setInputExtras(int)
+ * @see EditorInfo#extras
+ * @attr ref android.R.styleable#TextView_editorExtras
+ */
+ public Bundle getInputExtras (boolean create){
+ return mInputView.getInputExtras(create);
+ }
+
+ /**
+ * Get the type of the editable content.
+ *
+ * @see #setInputType(int)
+ * @see android.text.InputType
+ */
+ public int getInputType (){
+ return mInputView.getInputType();
+ }
+
+ /**
+ * @return the current key listener for this TextView.
+ * This will frequently be null for non-EditText TextViews.
+ *
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ */
+ public final KeyListener getKeyListener (){
+ return mInputView.getKeyListener();
+ }
+
+ /**
+ * @return the Layout that is currently being used to display the text.
+ * This can be null if the text or width has recently changes.
+ */
+ public final Layout getLayout (){
+ return mInputView.getLayout();
+ }
+
+ /**
+ * @return the extent by which text is currently being letter-spaced.
+ * This will normally be 0.
+ *
+ * @see #setLetterSpacing(float)
+ * @see android.graphics.Paint#setLetterSpacing
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public float getLetterSpacing (){
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? mInputView.getLetterSpacing() : 0;
+ }
+
+ /**
+ * Return the baseline for the specified line (0...getLineCount() - 1)
+ * If bounds is not null, return the top, left, right, bottom extents
+ * of the specified line in it. If the internal Layout has not been built,
+ * return 0 and set bounds to (0, 0, 0, 0)
+ * @param line which line to examine (0..getLineCount() - 1)
+ * @param bounds Optional. If not null, it returns the extent of the line
+ * @return the Y-coordinate of the baseline
+ */
+ public int getLineBounds (int line, Rect bounds){
+ return mInputView.getLineBounds(line, bounds);
+ }
+
+ /**
+ * Return the number of lines of text, or 0 if the internal Layout has not
+ * been built.
+ */
+ public int getLineCount (){
+ return mInputView.getLineCount();
+ }
+
+ /**
+ * @return the height of one standard line in pixels. Note that markup
+ * within the text can cause individual lines to be taller or shorter
+ * than this height, and the layout may contain additional first-
+ * or last-line padding.
+ */
+ public int getLineHeight (){
+ return mInputView.getLineHeight();
+ }
+
+ /**
+ * Gets the line spacing extra space
+ *
+ * @return the extra space that is added to the height of each lines of this TextView.
+ *
+ * @see #setLineSpacing(float, float)
+ * @see #getLineSpacingMultiplier()
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public float getLineSpacingExtra (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getLineSpacingExtra();
+ return 0f;
+ }
+
+ /**
+ * Gets the line spacing multiplier
+ *
+ * @return the value by which each line's height is multiplied to get its actual height.
+ *
+ * @see #setLineSpacing(float, float)
+ * @see #getLineSpacingExtra()
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public float getLineSpacingMultiplier (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getLineSpacingMultiplier();
+ return 0f;
+ }
+
+ /**
+ * @return the list of colors used to paint the links in the text, for the different states of
+ * this TextView
+ *
+ * @see #setLinkTextColor(ColorStateList)
+ * @see #setLinkTextColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final ColorStateList getLinkTextColors (){
+ return mInputView.getLinkTextColors();
+ }
+
+ /**
+ * Returns whether the movement method will automatically be set to
+ * {@link android.text.method.LinkMovementMethod} if {@link #setAutoLinkMask} has been
+ * set to nonzero and links are detected in {@link #setText}.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_linksClickable
+ */
+ public final boolean getLinksClickable (){
+ return mInputView.getLinksClickable();
+ }
+
+ /**
+ * Gets the number of times the marquee animation is repeated. Only meaningful if the
+ * TextView has marquee enabled.
+ *
+ * @return the number of times the marquee animation is repeated. -1 if the animation
+ * repeats indefinitely
+ *
+ * @see #setMarqueeRepeatLimit(int)
+ *
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMarqueeRepeatLimit (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMarqueeRepeatLimit();
+
+ return -1;
+ }
+
+ /**
+ * @return the maximum width of the TextView, expressed in ems or -1 if the maximum width
+ * was set in pixels instead (using {@link #setMaxWidth(int)} or {@link #setWidth(int)}).
+ *
+ * @see #setMaxEms(int)
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxEms
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMaxEms (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMaxEms();
+
+ return -1;
+ }
+
+ /**
+ * @return the maximum height of this TextView expressed in pixels, or -1 if the maximum
+ * height was set in number of lines instead using {@link #setMaxLines(int) or #setLines(int)}.
+ *
+ * @see #setMaxHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxHeight
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMaxHeight (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMaxHeight();
+
+ return -1;
+ }
+
+ /**
+ * @return the maximum number of lines displayed in this TextView, or -1 if the maximum
+ * height was set in pixels instead using {@link #setMaxHeight(int) or #setDividerHeight(int)}.
+ *
+ * @see #setMaxLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxLines
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMaxLines (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMaxLines();
+
+ return -1;
+ }
+
+ /**
+ * @return the maximum width of the TextView, in pixels or -1 if the maximum width
+ * was set in ems instead (using {@link #setMaxEms(int)} or {@link #setEms(int)}).
+ *
+ * @see #setMaxWidth(int)
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_maxWidth
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMaxWidth (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMaxWidth();
+
+ return -1;
+ }
+
+ /**
+ * @return the minimum width of the TextView, expressed in ems or -1 if the minimum width
+ * was set in pixels instead (using {@link #setMinWidth(int)} or {@link #setWidth(int)}).
+ *
+ * @see #setMinEms(int)
+ * @see #setEms(int)
+ *
+ * @attr ref android.R.styleable#TextView_minEms
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMinEms (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMinEms();
+
+ return -1;
+ }
+
+ /**
+ * @return the minimum height of this TextView expressed in pixels, or -1 if the minimum
+ * height was set in number of lines instead using {@link #setMinLines(int) or #setLines(int)}.
+ *
+ * @see #setMinHeight(int)
+ *
+ * @attr ref android.R.styleable#TextView_minHeight
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMinHeight (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMinHeight();
+
+ return -1;
+ }
+
+ /**
+ * @return the minimum number of lines displayed in this TextView, or -1 if the minimum
+ * height was set in pixels instead using {@link #setMinHeight(int) or #setDividerHeight(int)}.
+ *
+ * @see #setMinLines(int)
+ *
+ * @attr ref android.R.styleable#TextView_minLines
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMinLines (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMinLines();
+
+ return -1;
+ }
+
+ /**
+ * @return the minimum width of the TextView, in pixels or -1 if the minimum width
+ * was set in ems instead (using {@link #setMinEms(int)} or {@link #setEms(int)}).
+ *
+ * @see #setMinWidth(int)
+ * @see #setWidth(int)
+ *
+ * @attr ref android.R.styleable#TextView_minWidth
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getMinWidth (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getMinWidth();
+
+ return -1;
+ }
+
+ /**
+ * @return the movement method being used for this TextView.
+ * This will frequently be null for non-EditText TextViews.
+ */
+ public final MovementMethod getMovementMethod (){
+ return mInputView.getMovementMethod();
+ }
+
+ /**
+ * Get the character offset closest to the specified absolute position. A typical use case is to
+ * pass the result of {@link android.view.MotionEvent#getX()} and {@link android.view.MotionEvent#getY()} to this method.
+ *
+ * @param x The horizontal absolute position of a point on screen
+ * @param y The vertical absolute position of a point on screen
+ * @return the character offset for the character whose position is closest to the specified
+ * position. Returns -1 if there is no layout.
+ */
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public int getOffsetForPosition (float x, float y){
+ if (getLayout() == null) return -1;
+ final int line = getLineAtCoordinate(y);
+ final int offset = getOffsetAtCoordinate(line, x);
+ return offset;
+ }
+
+ protected float convertToLocalHorizontalCoordinate(float x) {
+ x -= getTotalPaddingLeft();
+ // Clamp the position to inside of the view.
+ x = Math.max(0.0f, x);
+ x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
+ x += getScrollX();
+ return x;
+ }
+
+ protected int getLineAtCoordinate(float y) {
+ y -= getTotalPaddingTop();
+ // Clamp the position to inside of the view.
+ y = Math.max(0.0f, y);
+ y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
+ y += getScrollY();
+ return getLayout().getLineForVertical((int) y);
+ }
+
+ protected int getOffsetAtCoordinate(int line, float x) {
+ x = convertToLocalHorizontalCoordinate(x);
+ return getLayout().getOffsetForHorizontal(line, x);
+ }
+
+ /**
+ * @return the base paint used for the text. Please use this only to
+ * consult the Paint's properties and not to change them.
+ */
+ public TextPaint getPaint (){
+ return mInputView.getPaint();
+ }
+
+ /**
+ * @return the flags on the Paint being used to display the text.
+ * @see android.graphics.Paint#getFlags
+ */
+ public int getPaintFlags (){
+ return mInputView.getPaintFlags();
+ }
+
+ /**
+ * Get the private type of the content.
+ *
+ * @see #setPrivateImeOptions(String)
+ * @see EditorInfo#privateImeOptions
+ */
+ public String getPrivateImeOptions (){
+ return mInputView.getPrivateImeOptions();
+ }
+
+ /**
+ * Convenience for {@link android.text.Selection#getSelectionEnd}.
+ */
+ public int getSelectionEnd (){
+ return mInputView.getSelectionEnd();
+ }
+
+ /**
+ * Convenience for {@link android.text.Selection#getSelectionStart}.
+ */
+ public int getSelectionStart (){
+ return mInputView.getSelectionStart();
+ }
+
+ /**
+ * @return the color of the shadow layer
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowColor
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public int getShadowColor (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getShadowColor();
+
+ return 0;
+ }
+
+ /**
+ * @return the horizontal offset of the shadow layer
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowDx
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public float getShadowDx (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getShadowDx();
+
+ return 0;
+ }
+
+ /**
+ * @return the vertical offset of the shadow layer
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowDy
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public float getShadowDy (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getShadowDy();
+
+ return 0;
+ }
+
+ /**
+ * Gets the radius of the shadow layer.
+ *
+ * @return the radius of the shadow layer. If 0, the shadow layer is not visible
+ *
+ * @see #setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public float getShadowRadius (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ return mInputView.getShadowRadius();
+
+ return 0;
+ }
+
+ /**
+ * Returns whether the soft input method will be made visible when this
+ * TextView gets focused. The default is true.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public final boolean getShowSoftInputOnFocus (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ return mInputView.getShowSoftInputOnFocus();
+ return true;
+ }
+
+ /**
+ * Gets the text colors for the different states (normal, selected, focused) of the TextView.
+ *
+ * @see #setTextColor(ColorStateList)
+ * @see #setTextColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public final ColorStateList getTextColors (){
+ return mInputView.getTextColors();
+ }
+
+ /**
+ * Get the default {@link Locale} of the text in this TextView.
+ * @return the default {@link Locale} of the text in this TextView.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public Locale getTextLocale (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ return mInputView.getTextLocale();
+
+ return Locale.getDefault();
+ }
+
+ /**
+ * @return the extent by which text is currently being stretched
+ * horizontally. This will usually be 1.
+ */
+ public float getTextScaleX (){
+ return mInputView.getTextScaleX();
+ }
+
+ /**
+ * @return the size (in pixels) of the default text size in this TextView.
+ */
+ public float getTextSize (){
+ return mInputView.getTextSize();
+ }
+
+ /**
+ * Returns the total bottom padding of the view, including the bottom
+ * Drawable if any, the extra space to keep more than maxLines
+ * from showing, and the vertical offset for gravity, if any.
+ */
+ public int getTotalPaddingBottom (){
+ return getPaddingBottom() + mInputView.getTotalPaddingBottom() + (mSupportMode != SUPPORT_MODE_NONE ? mSupportView.getHeight() : 0);
+ }
+
+ /**
+ * Returns the total end padding of the view, including the end
+ * Drawable if any.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public int getTotalPaddingEnd (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ return getPaddingEnd() + mInputView.getTotalPaddingEnd();
+
+ return getTotalPaddingRight();
+ }
+
+ /**
+ * Returns the total left padding of the view, including the left
+ * Drawable if any.
+ */
+ public int getTotalPaddingLeft (){
+ return getPaddingLeft() + mInputView.getTotalPaddingLeft();
+ }
+
+ /**
+ * Returns the total right padding of the view, including the right
+ * Drawable if any.
+ */
+ public int getTotalPaddingRight (){
+ return getPaddingRight() + mInputView.getTotalPaddingRight();
+ }
+
+ /**
+ * Returns the total start padding of the view, including the start
+ * Drawable if any.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public int getTotalPaddingStart (){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ return getPaddingStart() + mInputView.getTotalPaddingStart();
+
+ return getTotalPaddingLeft();
+ }
+
+ /**
+ * Returns the total top padding of the view, including the top
+ * Drawable if any, the extra space to keep more than maxLines
+ * from showing, and the vertical offset for gravity, if any.
+ */
+ public int getTotalPaddingTop (){
+ return getPaddingTop() + mInputView.getTotalPaddingTop() + (mLabelEnable ? mLabelView.getHeight() : 0);
+ }
+
+ /**
+ * @return the current transformation method for this TextView.
+ * This will frequently be null except for single-line and password
+ * fields.
+ *
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public final TransformationMethod getTransformationMethod (){
+ return mInputView.getTransformationMethod();
+ }
+
+ /**
+ * @return the current typeface and style in which the text is being
+ * displayed.
+ *
+ * @see #setTypeface(Typeface)
+ *
+ * @attr ref android.R.styleable#TextView_fontFamily
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public Typeface getTypeface (){
+ return mInputView.getTypeface();
+ }
+
+ /**
+ * Returns the list of URLSpans attached to the text
+ * (by {@link android.text.util.Linkify} or otherwise) if any. You can call
+ * {@link URLSpan#getURL} on them to find where they link to
+ * or use {@link android.text.Spanned#getSpanStart} and {@link android.text.Spanned#getSpanEnd}
+ * to find the region of the text they are attached to.
+ */
+ public URLSpan[] getUrls (){
+ return mInputView.getUrls();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean hasOverlappingRendering (){
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mInputView.hasOverlappingRendering();
+ }
+
+ /**
+ * Return true iff there is a selection inside this text view.
+ */
+ public boolean hasSelection (){
+ return mInputView.hasSelection();
+ }
+
+ /**
+ * @return whether or not the cursor is visible (assuming this TextView is editable)
+ *
+ * @see #setCursorVisible(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public boolean isCursorVisible (){
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN || mInputView.isCursorVisible();
+ }
+
+ /**
+ * Returns whether this text view is a current input method target. The
+ * default implementation just checks with {@link android.view.inputmethod.InputMethodManager}.
+ */
+ public boolean isInputMethodTarget (){
+ return mInputView.isInputMethodTarget();
+ }
+
+ /**
+ * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
+ * by the IME or by the spell checker as the user types. This is done by adding
+ * {@link android.text.style.SuggestionSpan}s to the text.
+ *
+ * When suggestions are enabled (default), this list of suggestions will be displayed when the
+ * user asks for them on these parts of the text. This value depends on the inputType of this
+ * TextView.
+ *
+ * The class of the input type must be {@link android.text.InputType#TYPE_CLASS_TEXT}.
+ *
+ * In addition, the type variation must be one of
+ * {@link android.text.InputType#TYPE_TEXT_VARIATION_NORMAL},
+ * {@link android.text.InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
+ * {@link android.text.InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
+ * {@link android.text.InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
+ * {@link android.text.InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
+ *
+ * And finally, the {@link android.text.InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must not be set.
+ *
+ * @return true if the suggestions popup window is enabled, based on the inputType.
+ */
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public boolean isSuggestionsEnabled (){
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && mInputView.isSuggestionsEnabled();
+ }
+
+ /**
+ *
+ * Returns the state of the {@code textIsSelectable} flag (See
+ * {@link #setTextIsSelectable setTextIsSelectable()}). Although you have to set this flag
+ * to allow users to select and copy text in a non-editable TextView, the content of an
+ * {@link EditText} can always be selected, independently of the value of this flag.
+ *
+ *
+ * @return True if the text displayed in this TextView can be selected by the user.
+ *
+ * @attr ref android.R.styleable#TextView_textIsSelectable
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public boolean isTextSelectable (){
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || mInputView.isTextSelectable();
+ }
+
+ /**
+ * Returns the length, in characters, of the text managed by this TextView
+ */
+ public int length (){
+ return mInputView.length();
+ }
+
+ /**
+ * Move the cursor, if needed, so that it is at an offset that is visible
+ * to the user. This will not move the cursor if it represents more than
+ * one character (a selection range). This will only work if the
+ * TextView contains spannable text; otherwise it will do nothing.
+ *
+ * @return True if the cursor was actually moved, false otherwise.
+ */
+ public boolean moveCursorToVisibleOffset (){
+ return mInputView.moveCursorToVisibleOffset();
+ }
+
+ /**
+ * Called by the framework in response to a text completion from
+ * the current input method, provided by it calling
+ * {@link InputConnection#commitCompletion
+ * InputConnection.commitCompletion()}. The default implementation does
+ * nothing; text views that are supporting auto-completion should override
+ * this to do their desired behavior.
+ *
+ * @param text The auto complete text the user has selected.
+ */
+ public void onCommitCompletion (CompletionInfo text){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ ((InternalEditText)mInputView).superOnCommitCompletion(text);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ ((InternalAutoCompleteTextView)mInputView).superOnCommitCompletion(text);
+ else
+ ((InternalMultiAutoCompleteTextView)mInputView).superOnCommitCompletion(text);
+ }
+
+ /**
+ * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+ * a dictionnary) from the current input method, provided by it calling
+ * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
+ * implementation flashes the background of the corrected word to provide feedback to the user.
+ *
+ * @param info The auto correct info about the text that was corrected.
+ */
+ public void onCommitCorrection (CorrectionInfo info){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ ((InternalEditText)mInputView).superOnCommitCorrection(info);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ ((InternalAutoCompleteTextView)mInputView).superOnCommitCorrection(info);
+ else
+ ((InternalMultiAutoCompleteTextView)mInputView).superOnCommitCorrection(info);
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection (EditorInfo outAttrs){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return ((InternalEditText)mInputView).superOnCreateInputConnection(outAttrs);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ return ((InternalAutoCompleteTextView)mInputView).superOnCreateInputConnection(outAttrs);
+ else
+ return ((InternalMultiAutoCompleteTextView)mInputView).superOnCreateInputConnection(outAttrs);
+ }
+
+ /**
+ * Called when an attached input method calls
+ * {@link InputConnection#performEditorAction(int)
+ * InputConnection.performEditorAction()}
+ * for this text view. The default implementation will call your action
+ * listener supplied to {@link #setOnEditorActionListener}, or perform
+ * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
+ * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
+ * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
+ * EditorInfo.IME_ACTION_DONE}.
+ *
+ *
For backwards compatibility, if no IME options have been set and the
+ * text view would not normally advance focus on enter, then
+ * the NEXT and DONE actions received here will be turned into an enter
+ * key down/up pair to go through the normal key handling.
+ *
+ * @param actionCode The code of the action being performed.
+ *
+ * @see #setOnEditorActionListener
+ */
+ public void onEditorAction (int actionCode){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ ((InternalEditText)mInputView).superOnEditorAction(actionCode);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ ((InternalAutoCompleteTextView)mInputView).superOnEditorAction(actionCode);
+ else
+ ((InternalMultiAutoCompleteTextView)mInputView).superOnEditorAction(actionCode);
+ }
+
+ @Override
+ public boolean onKeyDown (int keyCode, KeyEvent event){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return ((InternalEditText)mInputView).superOnKeyDown(keyCode, event);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ return ((InternalAutoCompleteTextView)mInputView).superOnKeyDown(keyCode, event);
+ else
+ return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple (int keyCode, int repeatCount, KeyEvent event){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return ((InternalEditText)mInputView).superOnKeyMultiple(keyCode, repeatCount, event);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ return ((InternalAutoCompleteTextView)mInputView).superOnKeyMultiple(keyCode, repeatCount, event);
+ else
+ return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyPreIme (int keyCode, KeyEvent event){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return ((InternalEditText)mInputView).superOnKeyPreIme(keyCode, event);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ return ((InternalAutoCompleteTextView)mInputView).superOnKeyPreIme(keyCode, event);
+ else
+ return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyShortcut (int keyCode, KeyEvent event){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return ((InternalEditText)mInputView).superOnKeyShortcut(keyCode, event);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ return ((InternalAutoCompleteTextView)mInputView).superOnKeyShortcut(keyCode, event);
+ else
+ return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyShortcut(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp (int keyCode, KeyEvent event){
+ if(mAutoCompleteMode == AUTOCOMPLETE_MODE_NONE)
+ return ((InternalEditText)mInputView).superOnKeyUp(keyCode, event);
+ else if(mAutoCompleteMode == AUTOCOMPLETE_MODE_SINGLE)
+ return ((InternalAutoCompleteTextView)mInputView).superOnKeyUp(keyCode, event);
+ else
+ return ((InternalMultiAutoCompleteTextView)mInputView).superOnKeyUp(keyCode, event);
+ }
+
+ public void setOnSelectionChangedListener(TextView.OnSelectionChangedListener listener){
+ mOnSelectionChangedListener = listener;
+ }
+
+ /**
+ * This method is called when the selection has changed, in case any
+ * subclasses would like to know.
+ *
+ * @param selStart The new selection start location.
+ * @param selEnd The new selection end location.
+ */
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ if(mInputView == null)
+ return;
+
+ if(mInputView instanceof InternalEditText)
+ ((InternalEditText)mInputView).superOnSelectionChanged(selStart, selEnd);
+ else if(mInputView instanceof InternalAutoCompleteTextView)
+ ((InternalAutoCompleteTextView)mInputView).superOnSelectionChanged(selStart, selEnd);
+ else
+ ((InternalMultiAutoCompleteTextView)mInputView).superOnSelectionChanged(selStart, selEnd);
+
+ if(mOnSelectionChangedListener != null)
+ mOnSelectionChangedListener.onSelectionChanged(this, selStart, selEnd);
+ }
+
+ /**
+ * Removes the specified TextWatcher from the list of those whose
+ * methods are called
+ * whenever this TextView's text changes.
+ */
+ public void removeTextChangedListener (TextWatcher watcher){
+ mInputView.removeTextChangedListener(watcher);
+ }
+
+ /**
+ * Sets the properties of this field to transform input to ALL CAPS
+ * display. This may use a "small caps" formatting if available.
+ * This setting will be ignored if this field is editable or selectable.
+ *
+ * This call replaces the current transformation method. Disabling this
+ * will not necessarily restore the previous behavior from before this
+ * was enabled.
+ *
+ * @see #setTransformationMethod(TransformationMethod)
+ * @attr ref android.R.styleable#TextView_textAllCaps
+ */
+ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public void setAllCaps (boolean allCaps){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+ mInputView.setAllCaps(allCaps);
+ }
+
+ /**
+ * Sets the autolink mask of the text. See {@link
+ * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+ * possible values.
+ *
+ * @attr ref android.R.styleable#TextView_autoLink
+ */
+ public final void setAutoLinkMask (int mask){
+ mInputView.setAutoLinkMask(mask);
+ }
+
+ /**
+ * Sets the size of the padding between the compound drawables and
+ * the text.
+ *
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ */
+ public void setCompoundDrawablePadding (int pad){
+ mInputView.setCompoundDrawablePadding(pad);
+ if(mDividerCompoundPadding) {
+ mDivider.setPadding(mInputView.getTotalPaddingLeft(), mInputView.getTotalPaddingRight());
+ if(mLabelEnable)
+ mLabelView.setPadding(mDivider.getPaddingLeft(), mLabelView.getPaddingTop(), mDivider.getPaddingRight(), mLabelView.getPaddingBottom());
+ if(mSupportMode != SUPPORT_MODE_NONE)
+ mSupportView.setPadding(mDivider.getPaddingLeft(), mSupportView.getPaddingTop(), mDivider.getPaddingRight(), mSupportView.getPaddingBottom());
+ }
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above, to the
+ * right of, and below the text. Use {@code null} if you do not want a
+ * Drawable there. The Drawables must already have had
+ * {@link Drawable#setBounds} called.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawablesRelative} or related methods.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawables (Drawable left, Drawable top, Drawable right, Drawable bottom){
+ mInputView.setCompoundDrawables(left, top, right, bottom);
+ if(mDividerCompoundPadding) {
+ mDivider.setPadding(mInputView.getTotalPaddingLeft(), mInputView.getTotalPaddingRight());
+ if(mLabelEnable)
+ mLabelView.setPadding(mDivider.getPaddingLeft(), mLabelView.getPaddingTop(), mDivider.getPaddingRight(), mLabelView.getPaddingBottom());
+ if(mSupportMode != SUPPORT_MODE_NONE)
+ mSupportView.setPadding(mDivider.getPaddingLeft(), mSupportView.getPaddingTop(), mDivider.getPaddingRight(), mSupportView.getPaddingBottom());
+ }
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the start of, above, to the end
+ * of, and below the text. Use {@code null} if you do not want a Drawable
+ * there. The Drawables must already have had {@link Drawable#setBounds}
+ * called.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawables} or related methods.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public void setCompoundDrawablesRelative (Drawable start, Drawable top, Drawable end, Drawable bottom){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ mInputView.setCompoundDrawablesRelative(start, top, end, bottom);
+ else
+ mInputView.setCompoundDrawables(start, top, end, bottom);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the start of, above, to the end
+ * of, and below the text. Use {@code null} if you do not want a Drawable
+ * there. The Drawables' bounds will be set to their intrinsic bounds.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawables} or related methods.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public void setCompoundDrawablesRelativeWithIntrinsicBounds (Drawable start, Drawable top, Drawable end, Drawable bottom){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ mInputView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+ else
+ mInputView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the start of, above, to the end
+ * of, and below the text. Use 0 if you do not want a Drawable there. The
+ * Drawables' bounds will be set to their intrinsic bounds.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawables} or related methods.
+ *
+ * @param start Resource identifier of the start Drawable.
+ * @param top Resource identifier of the top Drawable.
+ * @param end Resource identifier of the end Drawable.
+ * @param bottom Resource identifier of the bottom Drawable.
+ *
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public void setCompoundDrawablesRelativeWithIntrinsicBounds (int start, int top, int end, int bottom){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ mInputView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
+ else
+ mInputView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above, to the
+ * right of, and below the text. Use {@code null} if you do not want a
+ * Drawable there. The Drawables' bounds will be set to their intrinsic
+ * bounds.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawablesRelative} or related methods.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawablesWithIntrinsicBounds (Drawable left, Drawable top, Drawable right, Drawable bottom){
+ mInputView.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above, to the
+ * right of, and below the text. Use 0 if you do not want a Drawable there.
+ * The Drawables' bounds will be set to their intrinsic bounds.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * {@link #setCompoundDrawablesRelative} or related methods.
+ *
+ * @param left Resource identifier of the left Drawable.
+ * @param top Resource identifier of the top Drawable.
+ * @param right Resource identifier of the right Drawable.
+ * @param bottom Resource identifier of the bottom Drawable.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawablesWithIntrinsicBounds (int left, int top, int right, int bottom){
+ mInputView.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
+ }
+
+ /**
+ * Set whether the cursor is visible. The default is true. Note that this property only
+ * makes sense for editable TextView.
+ *
+ * @see #isCursorVisible()
+ *
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ */
+ public void setCursorVisible (boolean visible){
+ mInputView.setCursorVisible(visible);
+ }
+
+ /**
+ * If provided, this ActionMode.Callback will be used to create the ActionMode when text
+ * selection is initiated in this View.
+ *
+ * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
+ * Paste actions, depending on what this View supports.
+ *
+ * A custom implementation can add new entries in the default menu in its
+ * {@link ActionMode.Callback#onPrepareActionMode(ActionMode, android.view.Menu)} method. The
+ * default actions can also be removed from the menu using {@link android.view.Menu#removeItem(int)} and
+ * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy}
+ * or {@link android.R.id#paste} ids as parameters.
+ *
+ * Returning false from
+ * {@link ActionMode.Callback#onCreateActionMode(ActionMode, android.view.Menu)} will prevent
+ * the action mode from being started.
+ *
+ * Action click events should be handled by the custom implementation of
+ * {@link ActionMode.Callback#onActionItemClicked(ActionMode, android.view.MenuItem)}.
+ *
+ * Note that text selection mode is not started when a TextView receives focus and the
+ * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
+ * that case, to allow for quick replacement.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void setCustomSelectionActionModeCallback (ActionMode.Callback actionModeCallback){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ mInputView.setCustomSelectionActionModeCallback(actionModeCallback);
+ }
+
+ /**
+ * Sets the Factory used to create new Editables.
+ */
+ public final void setEditableFactory (Editable.Factory factory){
+ mInputView.setEditableFactory(factory);
+ }
+
+ /**
+ * Set the TextView's elegant height metrics flag. This setting selects font
+ * variants that have not been compacted to fit Latin-based vertical
+ * metrics, and also increases top and bottom bounds to provide more space.
+ *
+ * @param elegant set the paint's elegant metrics flag.
+ *
+ * @attr ref android.R.styleable#TextView_elegantTextHeight
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public void setElegantTextHeight (boolean elegant){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ mInputView.setElegantTextHeight(elegant);
+ }
+
+ /**
+ * Makes the TextView exactly this many ems wide
+ *
+ * @see #setMaxEms(int)
+ * @see #setMinEms(int)
+ * @see #getMinEms()
+ * @see #getMaxEms()
+ *
+ * @attr ref android.R.styleable#TextView_ems
+ */
+ public void setEms (int ems){
+ mInputView.setEms(ems);
+ }
+
+ /**
+ * Apply to this text view the given extracted text, as previously
+ * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
+ */
+ public void setExtractedText (ExtractedText text){
+ mInputView.setExtractedText(text);
+ }
+
+ /**
+ * Sets the list of input filters that will be used if the buffer is
+ * Editable. Has no effect otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_maxLength
+ */
+ public void setFilters (InputFilter[] filters){
+ mInputView.setFilters(filters);
+ }
+
+ /**
+ * Sets font feature settings. The format is the same as the CSS
+ * font-feature-settings attribute:
+ * http://dev.w3.org/csswg/css-fonts/#propdef-font-feature-settings
+ *
+ * @param fontFeatureSettings font feature settings represented as CSS compatible string
+ * @see #getFontFeatureSettings()
+ * @see android.graphics.Paint#getFontFeatureSettings
+ *
+ * @attr ref android.R.styleable#TextView_fontFeatureSettings
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public void setFontFeatureSettings (String fontFeatureSettings){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ mInputView.setFontFeatureSettings(fontFeatureSettings);
+ }
+
+ /**
+ * Control whether this text view saves its entire text contents when
+ * freezing to an icicle, in addition to dynamic state such as cursor
+ * position. By default this is false, not saving the text. Set to true
+ * if the text in the text view is not being saved somewhere else in
+ * persistent storage (such as in a content provider) so that if the
+ * view is later thawed the user will not lose their data.
+ *
+ * @param freezesText Controls whether a frozen icicle should include the
+ * entire text data: true to include it, false to not.
+ *
+ * @attr ref android.R.styleable#TextView_freezesText
+ */
+ public void setFreezesText (boolean freezesText){
+ mInputView.setFreezesText(freezesText);
+ }
+
+ /**
+ * Sets the horizontal alignment of the text and the
+ * vertical gravity that will be used when there is extra space
+ * in the TextView beyond what is required for the text itself.
+ *
+ * @see Gravity
+ * @attr ref android.R.styleable#TextView_gravity
+ */
+ public void setGravity (int gravity){
+ mInputView.setGravity(gravity);
+ }
+
+ /**
+ * Sets the color used to display the selection highlight.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ */
+ public void setHighlightColor (int color){
+ mInputView.setHighlightColor(color);
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty.
+ * Null means to use the normal empty text. The hint does not currently
+ * participate in determining the size of the view.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ public final void setHint (CharSequence hint){
+ mInputView.setHint(hint);
+ if(mLabelView != null)
+ mLabelView.setText(hint);
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty,
+ * from a resource.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ public final void setHint (int resid){
+ mInputView.setHint(resid);
+ if(mLabelView != null)
+ mLabelView.setText(resid);
+ }
+
+ /**
+ * Sets the color of the hint text.
+ *
+ * @see #getHintTextColors()
+ * @see #setHintTextColor(int)
+ * @see #setTextColor(ColorStateList)
+ * @see #setLinkTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final void setHintTextColor (ColorStateList colors){
+ mInputView.setHintTextColor(colors);
+ }
+
+ /**
+ * Sets the color of the hint text for all the states (disabled, focussed, selected...) of this
+ * TextView.
+ *
+ * @see #setHintTextColor(ColorStateList)
+ * @see #getHintTextColors()
+ * @see #setTextColor(int)
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final void setHintTextColor (int color){
+ mInputView.setHintTextColor(color);
+ }
+
+ /**
+ * Sets whether the text should be allowed to be wider than the
+ * View is. If false, it will be wrapped to the width of the View.
+ *
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ */
+ public void setHorizontallyScrolling (boolean whether){
+ mInputView.setHorizontallyScrolling(whether);
+ }
+
+ /**
+ * Change the custom IME action associated with the text view, which
+ * will be reported to an IME with {@link EditorInfo#actionLabel}
+ * and {@link EditorInfo#actionId} when it has focus.
+ * @see #getImeActionLabel
+ * @see #getImeActionId
+ * @see EditorInfo
+ * @attr ref android.R.styleable#TextView_imeActionLabel
+ * @attr ref android.R.styleable#TextView_imeActionId
+ */
+ public void setImeActionLabel (CharSequence label, int actionId){
+ mInputView.setImeActionLabel(label, actionId);
+ }
+
+ /**
+ * Change the editor type integer associated with the text view, which
+ * will be reported to an IME with {@link EditorInfo#imeOptions} when it
+ * has focus.
+ * @see #getImeOptions
+ * @see EditorInfo
+ * @attr ref android.R.styleable#TextView_imeOptions
+ */
+ public void setImeOptions (int imeOptions){
+ mInputView.setImeOptions(imeOptions);
+ }
+
+ /**
+ * Set whether the TextView includes extra top and bottom padding to make
+ * room for accents that go above the normal ascent and descent.
+ * The default is true.
+ *
+ * @see #getIncludeFontPadding()
+ *
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ */
+ public void setIncludeFontPadding (boolean includepad){
+ mInputView.setIncludeFontPadding(includepad);
+ }
+
+ /**
+ * Set the extra input data of the text, which is the
+ * {@link EditorInfo#extras TextBoxAttribute.extras}
+ * Bundle that will be filled in when creating an input connection. The
+ * given integer is the resource ID of an XML resource holding an
+ * {@link android.R.styleable#InputExtras <input-extras>} XML tree.
+ *
+ * @see #getInputExtras(boolean)
+ * @see EditorInfo#extras
+ * @attr ref android.R.styleable#TextView_editorExtras
+ */
+ public void setInputExtras (int xmlResId) throws XmlPullParserException, IOException{
+ mInputView.setInputExtras(xmlResId);
+ }
+
+ /**
+ * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
+ * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
+ * to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
+ * then a soft keyboard will not be displayed for this text view.
+ *
+ * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
+ * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
+ * type.
+ *
+ * @see #getInputType()
+ * @see #setRawInputType(int)
+ * @see android.text.InputType
+ * @attr ref android.R.styleable#TextView_inputType
+ */
+ public void setInputType (int type){
+ mInputView.setInputType(type);
+ }
+
+ /**
+ * Sets the key listener to be used with this TextView. This can be null
+ * to disallow user input. Note that this method has significant and
+ * subtle interactions with soft keyboards and other input method:
+ * see {@link KeyListener#getInputType() KeyListener.getContentType()}
+ * for important details. Calling this method will replace the current
+ * content type of the text view with the content type returned by the
+ * key listener.
+ *
+ * Be warned that if you want a TextView with a key listener or movement
+ * method not to be focusable, or if you want a TextView without a
+ * key listener or movement method to be focusable, you must call
+ * {@link #setFocusable} again after calling this to get the focusability
+ * back the way you want it.
+ *
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ */
+ public void setKeyListener (KeyListener input){
+ mInputView.setKeyListener(input);
+ }
+
+ /**
+ * Sets text letter-spacing. The value is in 'EM' units. Typical values
+ * for slight expansion will be around 0.05. Negative values tighten text.
+ *
+ * @see #getLetterSpacing()
+ * @see android.graphics.Paint#getLetterSpacing
+ *
+ * @attr ref android.R.styleable#TextView_letterSpacing
+ */
+ public void setLetterSpacing (float letterSpacing){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ mInputView.setLetterSpacing(letterSpacing);
+ }
+
+ /**
+ * Sets line spacing for this TextView. Each line will have its height
+ * multiplied by mult
and have add
added to it.
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+ public void setLineSpacing (float add, float mult){
+ mInputView.setLineSpacing(add, mult);
+ }
+
+ /**
+ * Makes the TextView exactly this many lines tall.
+ *
+ * Note that setting this value overrides any other (minimum / maximum) number of lines or
+ * height setting. A single line TextView will set this value to 1.
+ *
+ * @attr ref android.R.styleable#TextView_lines
+ */
+ public void setLines (int lines){
+ mInputView.setLines(lines);
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @see #setLinkTextColor(int)
+ * @see #getLinkTextColors()
+ * @see #setTextColor(ColorStateList)
+ * @see #setHintTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final void setLinkTextColor (ColorStateList colors){
+ mInputView.setLinkTextColor(colors);
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @see #setLinkTextColor(int)
+ * @see #getLinkTextColors()
+ * @see #setTextColor(ColorStateList)
+ * @see #setHintTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final void setLinkTextColor (int color){
+ mInputView.setLinkTextColor(color);
+ }
+
+ /**
+ * Sets whether the movement method will automatically be set to
+ * {@link android.text.method.LinkMovementMethod} if {@link #setAutoLinkMask} has been
+ * set to nonzero and links are detected in {@link #setText}.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_linksClickable
+ */
+ public final void setLinksClickable (boolean whether){
+ mInputView.setLinksClickable(whether);
+ }
+
+ /**
+ * Sets how many times to repeat the marquee animation. Only applied if the
+ * TextView has marquee enabled. Set to -1 to repeat indefinitely.
+ *
+ * @see #getMarqueeRepeatLimit()
+ *
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ */
+ public void setMarqueeRepeatLimit (int marqueeLimit){
+ mInputView.setMarqueeRepeatLimit(marqueeLimit);
+ }
+
+ /**
+ * Makes the TextView at most this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_maxEms
+ */
+ public void setMaxEms (int maxems){
+ mInputView.setMaxEms(maxems);
+ }
+
+ /**
+ * Makes the TextView at most this many pixels tall. This option is mutually exclusive with the
+ * {@link #setMaxLines(int)} method.
+ *
+ * Setting this value overrides any other (maximum) number of lines setting.
+ *
+ * @attr ref android.R.styleable#TextView_maxHeight
+ */
+ public void setMaxHeight (int maxHeight){
+ mInputView.setMaxHeight(maxHeight);
+ }
+
+ /**
+ * Makes the TextView at most this many lines tall.
+ *
+ * Setting this value overrides any other (maximum) height setting.
+ *
+ * @attr ref android.R.styleable#TextView_maxLines
+ */
+ public void setMaxLines (int maxlines){
+ mInputView.setMaxLines(maxlines);
+ }
+
+ /**
+ * Makes the TextView at most this many pixels wide
+ *
+ * @attr ref android.R.styleable#TextView_maxWidth
+ */
+ public void setMaxWidth (int maxpixels){
+ mInputView.setMaxWidth(maxpixels);
+ }
+
+ /**
+ * Makes the TextView at least this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_minEms
+ */
+ public void setMinEms (int minems){
+ mInputView.setMinEms(minems);
+ }
+
+ /**
+ * Makes the TextView at least this many pixels tall.
+ *
+ * Setting this value overrides any other (minimum) number of lines setting.
+ *
+ * @attr ref android.R.styleable#TextView_minHeight
+ */
+ public void setMinHeight (int minHeight){
+ mInputView.setMinHeight(minHeight);
+ }
+
+ /**
+ * Makes the TextView at least this many lines tall.
+ *
+ * Setting this value overrides any other (minimum) height setting. A single line TextView will
+ * set this value to 1.
+ *
+ * @see #getMinLines()
+ *
+ * @attr ref android.R.styleable#TextView_minLines
+ */
+ public void setMinLines (int minlines){
+ mInputView.setMinLines(minlines);
+ }
+
+ /**
+ * Makes the TextView at least this many pixels wide
+ *
+ * @attr ref android.R.styleable#TextView_minWidth
+ */
+ public void setMinWidth (int minpixels){
+ mInputView.setMinWidth(minpixels);
+ }
+
+ /**
+ * Sets the movement method (arrow key handler) to be used for
+ * this TextView. This can be null to disallow using the arrow keys
+ * to move the cursor or scroll the view.
+ *
+ * Be warned that if you want a TextView with a key listener or movement
+ * method not to be focusable, or if you want a TextView without a
+ * key listener or movement method to be focusable, you must call
+ * {@link #setFocusable} again after calling this to get the focusability
+ * back the way you want it.
+ */
+ public final void setMovementMethod (MovementMethod movement){
+ mInputView.setMovementMethod(movement);
+ }
+
+ /**
+ * Set a special listener to be called when an action is performed
+ * on the text view. This will be called when the enter key is pressed,
+ * or when an action supplied to the IME is selected by the user. Setting
+ * this means that the normal hard key event will not insert a newline
+ * into the text view, even if it is multi-line; holding down the ALT
+ * modifier will, however, allow the user to insert a newline character.
+ */
+ public void setOnEditorActionListener (TextView.OnEditorActionListener l){
+ mInputView.setOnEditorActionListener(l);
+ }
+
+ /**
+ * Register a callback to be invoked when a hardware key is pressed in this view.
+ * Key presses in software input methods will generally not trigger the methods of
+ * this listener.
+ * @param l the key listener to attach to this view
+ */
+ @Override
+ public void setOnKeyListener(OnKeyListener l) {
+ mInputView.setOnKeyListener(l);
+ }
+
+ /**
+ * Register a callback to be invoked when focus of this view changed.
+ *
+ * @param l The callback that will run.
+ */
+ @Override
+ public void setOnFocusChangeListener(OnFocusChangeListener l) {
+ mInputView.setOnFocusChangeListener(l);
+ }
+
+ /**
+ * Directly change the content type integer of the text view, without
+ * modifying any other state.
+ * @see #setInputType(int)
+ * @see android.text.InputType
+ * @attr ref android.R.styleable#TextView_inputType
+ */
+ public void setRawInputType (int type){
+ mInputView.setRawInputType(type);
+ }
+
+ public void setScroller (Scroller s){
+ mInputView.setScroller(s);
+ }
+
+ /**
+ * Set the TextView so that when it takes focus, all the text is
+ * selected.
+ *
+ * @attr ref android.R.styleable#TextView_selectAllOnFocus
+ */
+ public void setSelectAllOnFocus (boolean selectAllOnFocus){
+ mInputView.setSelectAllOnFocus(selectAllOnFocus);
+ }
+
+ @Override
+ public void setSelected (boolean selected){
+ mInputView.setSelected(selected);
+ }
+
+ /**
+ * Gives the text a shadow of the specified blur radius and color, the specified
+ * distance from its drawn position.
+ *
+ * The text shadow produced does not interact with the properties on view
+ * that are responsible for real time shadows,
+ * {@link View#getElevation() elevation} and
+ * {@link View#getTranslationZ() translationZ}.
+ *
+ * @see android.graphics.Paint#setShadowLayer(float, float, float, int)
+ *
+ * @attr ref android.R.styleable#TextView_shadowColor
+ * @attr ref android.R.styleable#TextView_shadowDx
+ * @attr ref android.R.styleable#TextView_shadowDy
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ */
+ public void setShadowLayer (float radius, float dx, float dy, int color){
+ mInputView.setShadowLayer(radius, dx, dy, color);
+ }
+
+ /**
+ * Sets whether the soft input method will be made visible when this
+ * TextView gets focused. The default is true.
+ */
+ public final void setShowSoftInputOnFocus (boolean show){
+ mInputView.setShowSoftInputOnFocus(show);
+ }
+
+ /**
+ * Sets the properties of this field (lines, horizontally scrolling,
+ * transformation method) to be for a single-line input.
+ *
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public void setSingleLine (){
+ mInputView.setSingleLine();
+ }
+
+ /**
+ * Sets the Factory used to create new Spannables.
+ */
+ public final void setSpannableFactory (Spannable.Factory factory){
+ mInputView.setSpannableFactory(factory);
+ }
+
+ public final void setText (int resid){
+ mInputView.setText(resid);
+ }
+
+ public final void setText (char[] text, int start, int len){
+ mInputView.setText(text, start, len);
+ }
+
+ public final void setText (int resid, TextView.BufferType type){
+ mInputView.setText(resid, type);
+ }
+
+ public final void setText (CharSequence text){
+ mInputView.setText(text);
+ }
+
+ /**
+ * Sets the text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ */
+ public void setTextAppearance (Context context, int resid){
+ mInputView.setTextAppearance(context, resid);
+ }
+
+ /**
+ * Sets the text color.
+ *
+ * @see #setTextColor(int)
+ * @see #getTextColors()
+ * @see #setHintTextColor(ColorStateList)
+ * @see #setLinkTextColor(ColorStateList)
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public void setTextColor (ColorStateList colors){
+ mInputView.setTextColor(colors);
+ }
+
+ /**
+ * Sets the text color for all the states (normal, selected,
+ * focused) to be this color.
+ *
+ * @see #setTextColor(ColorStateList)
+ * @see #getTextColors()
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public void setTextColor (int color){
+ mInputView.setTextColor(color);
+ }
+
+ /**
+ * Sets whether the content of this view is selectable by the user. The default is
+ * {@code false}, meaning that the content is not selectable.
+ *
+ * When you use a TextView to display a useful piece of information to the user (such as a
+ * contact's address), make it selectable, so that the user can select and copy its
+ * content. You can also use set the XML attribute
+ * {@link android.R.styleable#TextView_textIsSelectable} to "true".
+ *
+ * When you call this method to set the value of {@code textIsSelectable}, it sets
+ * the flags {@code focusable}, {@code focusableInTouchMode}, {@code clickable},
+ * and {@code longClickable} to the same value. These flags correspond to the attributes
+ * {@link android.R.styleable#View_focusable android:focusable},
+ * {@link android.R.styleable#View_focusableInTouchMode android:focusableInTouchMode},
+ * {@link android.R.styleable#View_clickable android:clickable}, and
+ * {@link android.R.styleable#View_longClickable android:longClickable}. To restore any of these
+ * flags to a state you had set previously, call one or more of the following methods:
+ * {@link #setFocusable(boolean) setFocusable()},
+ * {@link #setFocusableInTouchMode(boolean) setFocusableInTouchMode()},
+ * {@link #setClickable(boolean) setClickable()} or
+ * {@link #setLongClickable(boolean) setLongClickable()}.
+ *
+ * @param selectable Whether the content of this TextView should be selectable.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void setTextIsSelectable (boolean selectable){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ mInputView.setTextIsSelectable(selectable);
+ }
+
+ /**
+ * Like {@link #setText(CharSequence)},
+ * except that the cursor position (if any) is retained in the new text.
+ *
+ * @param text The new text to place in the text view.
+ *
+ * @see #setText(CharSequence)
+ */
+ public final void setTextKeepState (CharSequence text){
+ mInputView.setTextKeepState(text);
+ }
+
+ /**
+ * Like {@link #setText(CharSequence, TextView.BufferType)},
+ * except that the cursor position (if any) is retained in the new text.
+ *
+ * @see #setText(CharSequence, TextView.BufferType)
+ */
+ public final void setTextKeepState (CharSequence text, TextView.BufferType type){
+ mInputView.setTextKeepState(text, type);
+ }
+
+ /**
+ * Set the default {@link Locale} of the text in this TextView to the given value. This value
+ * is used to choose appropriate typefaces for ambiguous characters. Typically used for CJK
+ * locales to disambiguate Hanzi/Kanji/Hanja characters.
+ *
+ * @param locale the {@link Locale} for drawing text, must not be null.
+ *
+ * @see android.graphics.Paint#setTextLocale
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public void setTextLocale (Locale locale){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ mInputView.setTextLocale(locale);
+ }
+
+ /**
+ * Sets the extent by which text should be stretched horizontally.
+ *
+ * @attr ref android.R.styleable#TextView_textScaleX
+ */
+ public void setTextScaleX (float size){
+ mInputView.setTextScaleX(size);
+ }
+
+ /**
+ * Set the default text size to the given value, interpreted as "scaled
+ * pixel" units. This size is adjusted based on the current density and
+ * user font size preference.
+ *
+ * @param size The scaled pixel size.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ public void setTextSize (float size){
+ mInputView.setTextSize(size);
+ }
+
+ /**
+ * Set the default text size to a given unit and value. See {@link
+ * TypedValue} for the possible dimension units.
+ *
+ * @param unit The desired dimension unit.
+ * @param size The desired size in the given units.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ public void setTextSize (int unit, float size){
+ mInputView.setTextSize(unit, size);
+ }
+
+ /**
+ * Sets the transformation that is applied to the text that this
+ * TextView is displaying.
+ *
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public final void setTransformationMethod (TransformationMethod method){
+ mInputView.setTransformationMethod(method);
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed,
+ * and turns on the fake bold and italic bits in the Paint if the
+ * Typeface that you provided does not have all the bits in the
+ * style that you specified.
+ *
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public void setTypeface (Typeface tf, int style){
+ mInputView.setTypeface(tf, style);
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed.
+ * Note that not all Typeface families actually have bold and italic
+ * variants, so you may need to use
+ * {@link #setTypeface(Typeface, int)} to get the appearance
+ * that you actually want.
+ *
+ * @see #getTypeface()
+ *
+ * @attr ref android.R.styleable#TextView_fontFamily
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public void setTypeface (Typeface tf){
+ mInputView.setTypeface(tf);
+ }
+
+ /**
+ * It would be better to rely on the input type for everything. A password inputType should have
+ * a password transformation. We should hence use isPasswordInputType instead of this method.
+ *
+ * We should:
+ * - Call setInputType in setKeyListener instead of changing the input type directly (which
+ * would install the correct transformation).
+ * - Refuse the installation of a non-password transformation in setTransformation if the input
+ * type is password.
+ *
+ * However, this is like this for legacy reasons and we cannot break existing apps. This method
+ * is useful since it matches what the user can see (obfuscated text or not).
+ *
+ * @return true if the current transformation method is of the password type.
+ */
+ private boolean hasPasswordTransformationMethod() {
+ return getTransformationMethod() != null && getTransformationMethod() instanceof PasswordTransformationMethod;
+ }
+
+ public boolean canCut() {
+ return !hasPasswordTransformationMethod() && getText().length() > 0 && hasSelection() && getKeyListener() != null;
+ }
+
+ public boolean canCopy() {
+ return !hasPasswordTransformationMethod() && getText().length() > 0 && hasSelection();
+ }
+
+ public boolean canPaste() {
+ return (getKeyListener() != null &&
+ getSelectionStart() >= 0 &&
+ getSelectionEnd() >= 0 &&
+ ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).hasPrimaryClip());
+ }
+
+ /* Inner class */
+
+ private class InputTextWatcher implements TextWatcher {
+ @Override
+ public void afterTextChanged(Editable s) {
+ int count = s.length();
+ setLabelVisible(count != 0, true);
+ if(mSupportMode == SUPPORT_MODE_CHAR_COUNTER)
+ updateCharCounter(count);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ }
+
+ private class LabelView extends TextView{
+
+ public LabelView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void setTextAppearance(int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void setTextAppearance(Context context, int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ return mInputView.getDrawableState();
+ }
+
+ }
+
+ private class InternalEditText extends android.widget.EditText{
+
+ public InternalEditText(Context context) {
+ super(context);
+ }
+
+ public InternalEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InternalEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setTextAppearance(int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void setTextAppearance(Context context, int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void refreshDrawableState() {
+ super.refreshDrawableState();
+
+ if(mLabelView != null)
+ mLabelView.refreshDrawableState();
+
+ if(mSupportView != null)
+ mSupportView.refreshDrawableState();
+ }
+
+ @Override
+ public void onCommitCompletion(CompletionInfo text) {
+ EditText.this.onCommitCompletion(text);
+ }
+
+ @Override
+ public void onCommitCorrection(CorrectionInfo info) {
+ EditText.this.onCommitCorrection(info);
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return EditText.this.onCreateInputConnection(outAttrs);
+ }
+
+ @Override
+ public void onEditorAction(int actionCode) {
+ EditText.this.onEditorAction(actionCode);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return EditText.this.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyShortcut(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ EditText.this.onSelectionChanged(selStart, selEnd);
+ }
+
+ void superOnCommitCompletion(CompletionInfo text) {
+ super.onCommitCompletion(text);
+ }
+
+ void superOnCommitCorrection(CorrectionInfo info) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ super.onCommitCorrection(info);
+ }
+
+ InputConnection superOnCreateInputConnection(EditorInfo outAttrs) {
+ return super.onCreateInputConnection(outAttrs);
+ }
+
+ void superOnEditorAction(int actionCode) {
+ super.onEditorAction(actionCode);
+ }
+
+ boolean superOnKeyDown(int keyCode, KeyEvent event) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ boolean superOnKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return super.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ boolean superOnKeyPreIme(int keyCode, KeyEvent event) {
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ boolean superOnKeyShortcut(int keyCode, KeyEvent event) {
+ return super.onKeyShortcut(keyCode, event);
+ }
+
+ boolean superOnKeyUp(int keyCode, KeyEvent event) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ void superOnSelectionChanged(int selStart, int selEnd) {
+ super.onSelectionChanged(selStart, selEnd);
+ }
+ }
+
+ private class InternalAutoCompleteTextView extends AutoCompleteTextView{
+
+ public InternalAutoCompleteTextView(Context context) {
+ super(context);
+ }
+
+ public InternalAutoCompleteTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InternalAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setTextAppearance(int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void setTextAppearance(Context context, int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void refreshDrawableState() {
+ super.refreshDrawableState();
+
+ if(mLabelView != null)
+ mLabelView.refreshDrawableState();
+
+ if(mSupportView != null)
+ mSupportView.refreshDrawableState();
+ }
+
+ @Override
+ public void onCommitCompletion(CompletionInfo text) {
+ EditText.this.onCommitCompletion(text);
+ }
+
+ @Override
+ public void onCommitCorrection(CorrectionInfo info) {
+ EditText.this.onCommitCorrection(info);
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return EditText.this.onCreateInputConnection(outAttrs);
+ }
+
+ @Override
+ public void onEditorAction(int actionCode) {
+ EditText.this.onEditorAction(actionCode);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return EditText.this.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyShortcut(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ EditText.this.onSelectionChanged(selStart, selEnd);
+ }
+
+ @Override
+ protected CharSequence convertSelectionToString(Object selectedItem) {
+ return EditText.this.convertSelectionToString(selectedItem);
+ }
+
+ @Override
+ protected void performFiltering(CharSequence text, int keyCode) {
+ EditText.this.performFiltering(text, keyCode);
+ }
+
+ @Override
+ protected void replaceText(CharSequence text) {
+ EditText.this.replaceText(text);
+ }
+
+ @Override
+ protected Filter getFilter() {
+ return EditText.this.getFilter();
+ }
+
+ @Override
+ public void onFilterComplete(int count) {
+ EditText.this.onFilterComplete(count);
+ }
+
+ void superOnCommitCompletion(CompletionInfo text) {
+ super.onCommitCompletion(text);
+ }
+
+ void superOnCommitCorrection(CorrectionInfo info) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ super.onCommitCorrection(info);
+ }
+
+ InputConnection superOnCreateInputConnection(EditorInfo outAttrs) {
+ return super.onCreateInputConnection(outAttrs);
+ }
+
+ void superOnEditorAction(int actionCode) {
+ super.onEditorAction(actionCode);
+ }
+
+ boolean superOnKeyDown(int keyCode, KeyEvent event) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ boolean superOnKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return super.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ boolean superOnKeyPreIme(int keyCode, KeyEvent event) {
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ boolean superOnKeyShortcut(int keyCode, KeyEvent event) {
+ return super.onKeyShortcut(keyCode, event);
+ }
+
+ boolean superOnKeyUp(int keyCode, KeyEvent event) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ void superOnFilterComplete(int count) {
+ super.onFilterComplete(count);
+ }
+
+ CharSequence superConvertSelectionToString(Object selectedItem) {
+ return super.convertSelectionToString(selectedItem);
+ }
+
+ void superPerformFiltering(CharSequence text, int keyCode) {
+ super.performFiltering(text, keyCode);
+ }
+
+ void superReplaceText(CharSequence text) {
+ super.replaceText(text);
+ }
+
+ Filter superGetFilter() {
+ return super.getFilter();
+ }
+
+ void superOnSelectionChanged(int selStart, int selEnd) {
+ super.onSelectionChanged(selStart, selEnd);
+ }
+ }
+
+ private class InternalMultiAutoCompleteTextView extends MultiAutoCompleteTextView{
+
+ public InternalMultiAutoCompleteTextView(Context context) {
+ super(context);
+ }
+
+ public InternalMultiAutoCompleteTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public InternalMultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setTextAppearance(int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void setTextAppearance(Context context, int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+ @Override
+ public void refreshDrawableState() {
+ super.refreshDrawableState();
+
+ if(mLabelView != null)
+ mLabelView.refreshDrawableState();
+
+ if(mSupportView != null)
+ mSupportView.refreshDrawableState();
+ }
+
+ @Override
+ public void onCommitCompletion(CompletionInfo text) {
+ EditText.this.onCommitCompletion(text);
+ }
+
+ @Override
+ public void onCommitCorrection(CorrectionInfo info) {
+ EditText.this.onCommitCorrection(info);
+ }
+
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return EditText.this.onCreateInputConnection(outAttrs);
+ }
+
+ @Override
+ public void onEditorAction(int actionCode) {
+ EditText.this.onEditorAction(actionCode);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return EditText.this.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyPreIme(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyShortcut(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return EditText.this.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ EditText.this.onSelectionChanged(selStart, selEnd);
+ }
+
+ @Override
+ public void onFilterComplete(int count) {
+ EditText.this.onFilterComplete(count);
+ }
+
+ @Override
+ protected CharSequence convertSelectionToString(Object selectedItem) {
+ return EditText.this.convertSelectionToString(selectedItem);
+ }
+
+ @Override
+ protected void performFiltering(CharSequence text, int keyCode) {
+ EditText.this.performFiltering(text, keyCode);
+ }
+
+ @Override
+ protected void replaceText(CharSequence text) {
+ EditText.this.replaceText(text);
+ }
+
+ @Override
+ protected Filter getFilter() {
+ return EditText.this.getFilter();
+ }
+
+ @Override
+ protected void performFiltering(CharSequence text, int start, int end, int keyCode){
+ EditText.this.performFiltering(text, start, end, keyCode);
+ }
+
+ void superOnCommitCompletion(CompletionInfo text) {
+ super.onCommitCompletion(text);
+ }
+
+ void superOnCommitCorrection(CorrectionInfo info) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ super.onCommitCorrection(info);
+ }
+
+ InputConnection superOnCreateInputConnection(EditorInfo outAttrs) {
+ return super.onCreateInputConnection(outAttrs);
+ }
+
+ void superOnEditorAction(int actionCode) {
+ super.onEditorAction(actionCode);
+ }
+
+ boolean superOnKeyDown(int keyCode, KeyEvent event) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ boolean superOnKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return super.onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ boolean superOnKeyPreIme(int keyCode, KeyEvent event) {
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ boolean superOnKeyShortcut(int keyCode, KeyEvent event) {
+ return super.onKeyShortcut(keyCode, event);
+ }
+
+ boolean superOnKeyUp(int keyCode, KeyEvent event) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ void superOnFilterComplete(int count) {
+ super.onFilterComplete(count);
+ }
+
+ CharSequence superConvertSelectionToString(Object selectedItem) {
+ return super.convertSelectionToString(selectedItem);
+ }
+
+ void superPerformFiltering(CharSequence text, int keyCode) {
+ super.performFiltering(text, keyCode);
+ }
+
+ void superReplaceText(CharSequence text) {
+ super.replaceText(text);
+ }
+
+ Filter superGetFilter() {
+ return super.getFilter();
+ }
+
+ void superPerformFiltering(CharSequence text, int start, int end, int keyCode){
+ super.performFiltering(text, start, end, keyCode);
+ }
+
+ void superOnSelectionChanged(int selStart, int selEnd) {
+ super.onSelectionChanged(selStart, selEnd);
+ }
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/FloatingActionButton.java b/material/src/main/java/com/rey/material/widget/FloatingActionButton.java
new file mode 100644
index 0000000..6500149
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/FloatingActionButton.java
@@ -0,0 +1,664 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.LineMorphingDrawable;
+import com.rey.material.drawable.OvalShadowDrawable;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.ViewUtil;
+
+@SuppressWarnings("unused")
+public class FloatingActionButton extends View implements ThemeManager.OnThemeChangedListener {
+
+ private OvalShadowDrawable mBackground;
+ private Drawable mIcon;
+ private Drawable mPrevIcon;
+ private int mAnimDuration = -1;
+ private Interpolator mInterpolator;
+ private SwitchIconAnimator mSwitchIconAnimator;
+ private int mIconSize = -1;
+
+ private RippleManager mRippleManager;
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ public static FloatingActionButton make(Context context, int resId) {
+ return new FloatingActionButton(context, null, resId);
+ }
+
+ public FloatingActionButton(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public FloatingActionButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ setClickable(true);
+ mSwitchIconAnimator = new SwitchIconAnimator();
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ if (!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId) {
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, defStyleRes);
+
+ int radius = -1;
+ int elevation = -1;
+ ColorStateList bgColor = null;
+ int bgAnimDuration = -1;
+ int iconSrc = 0;
+ int iconLineMorphing = 0;
+
+ for (int i = 0, count = a.getIndexCount(); i < count; i++) {
+ int attr = a.getIndex(i);
+
+ if (attr == R.styleable.FloatingActionButton_fab_radius)
+ radius = a.getDimensionPixelSize(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_elevation)
+ elevation = a.getDimensionPixelSize(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_backgroundColor)
+ bgColor = a.getColorStateList(attr);
+ else if (attr == R.styleable.FloatingActionButton_fab_backgroundAnimDuration)
+ bgAnimDuration = a.getInteger(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_iconSrc)
+ iconSrc = a.getResourceId(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_iconLineMorphing)
+ iconLineMorphing = a.getResourceId(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_iconSize)
+ mIconSize = a.getDimensionPixelSize(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_animDuration)
+ mAnimDuration = a.getInteger(attr, 0);
+ else if (attr == R.styleable.FloatingActionButton_fab_interpolator) {
+ int resId = a.getResourceId(R.styleable.FloatingActionButton_fab_interpolator, 0);
+ if (resId != 0)
+ mInterpolator = AnimationUtils.loadInterpolator(context, resId);
+ }
+ }
+
+ a.recycle();
+
+ if (mIconSize < 0)
+ mIconSize = ThemeUtil.dpToPx(context, 24);
+
+ if (mAnimDuration < 0)
+ mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+
+ if (mInterpolator == null)
+ mInterpolator = new DecelerateInterpolator();
+
+ if (mBackground == null) {
+ if (radius < 0)
+ radius = ThemeUtil.dpToPx(context, 28);
+
+ if (elevation < 0)
+ elevation = ThemeUtil.dpToPx(context, 4);
+
+ if (bgColor == null)
+ bgColor = ColorStateList.valueOf(ThemeUtil.colorAccent(context, 0));
+
+ if (bgAnimDuration < 0)
+ bgAnimDuration = 0;
+
+ mBackground = new OvalShadowDrawable(radius, bgColor, elevation, elevation, bgAnimDuration);
+ mBackground.setInEditMode(isInEditMode());
+ mBackground.setBounds(0, 0, getWidth(), getHeight());
+ mBackground.setCallback(this);
+ } else {
+ if (radius >= 0)
+ mBackground.setRadius(radius);
+
+ if (bgColor != null)
+ mBackground.setColor(bgColor);
+
+ if (elevation >= 0)
+ mBackground.setShadow(elevation, elevation);
+
+ if (bgAnimDuration >= 0)
+ mBackground.setAnimationDuration(bgAnimDuration);
+ }
+
+ if (iconLineMorphing != 0)
+ setIcon(new LineMorphingDrawable.Builder(context, iconLineMorphing).build(), false);
+ else if (iconSrc != 0) {
+ setIcon(getDrawable(iconSrc), false);
+ }
+
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+ Drawable background = getBackground();
+ if (background != null && background instanceof RippleDrawable) {
+ RippleDrawable drawable = (RippleDrawable) background;
+ drawable.setBackgroundDrawable(null);
+ drawable.setMask(RippleDrawable.Mask.TYPE_OVAL, 0, 0, 0, 0, (int) mBackground.getPaddingLeft(), (int) mBackground.getPaddingTop(), (int) mBackground.getPaddingRight(), (int) mBackground.getPaddingBottom());
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private Drawable getDrawable(int iconSrc) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return getContext().getResources().getDrawable(iconSrc, null);
+ }
+ //noinspection deprecation
+ return getContext().getResources().getDrawable(iconSrc);
+ }
+
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if (mCurrentStyle != style) {
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if (mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+
+ /**
+ * @return The radius of the button.
+ */
+ public int getRadius() {
+ return mBackground.getRadius();
+ }
+
+ /**
+ * Set radius of the button.
+ *
+ * @param radius The radius in pixel.
+ */
+ public void setRadius(int radius) {
+ if (mBackground.setRadius(radius))
+ requestLayout();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public float getElevation() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ return super.getElevation();
+
+ return mBackground.getShadowSize();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void setElevation(float elevation) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+ super.setElevation(elevation);
+ else if (mBackground.setShadow(elevation, elevation))
+ requestLayout();
+ }
+
+ /**
+ * @return The line state of LineMorphingDrawable that is used as this button's icon.
+ */
+ public int getLineMorphingState() {
+ if (mIcon != null && mIcon instanceof LineMorphingDrawable)
+ return ((LineMorphingDrawable) mIcon).getLineState();
+
+ return -1;
+ }
+
+ /**
+ * Set the line state of LineMorphingDrawable that is used as this button's icon.
+ *
+ * @param state The line state.
+ * @param animation Indicate should show animation when switch line state or not.
+ */
+ public void setLineMorphingState(int state, boolean animation) {
+ if (mIcon != null && mIcon instanceof LineMorphingDrawable)
+ ((LineMorphingDrawable) mIcon).switchLineState(state, animation);
+ }
+
+ /**
+ * @return The background color of this button.
+ */
+ public ColorStateList getBackgroundColor() {
+ return mBackground.getColor();
+ }
+
+ /**
+ * @return The drawable is used as this button's icon.
+ */
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Set the drawable that is used as this button's icon.
+ *
+ * @param icon The drawable.
+ * @param animation Indicate should show animation when switch drawable or not.
+ */
+ public void setIcon(Drawable icon, boolean animation) {
+ if (icon == null)
+ return;
+
+ if (animation) {
+ mSwitchIconAnimator.startAnimation(icon);
+ invalidate();
+ } else {
+ if (mIcon != null) {
+ mIcon.setCallback(null);
+ unscheduleDrawable(mIcon);
+ }
+
+ mIcon = icon;
+ float half = mIconSize / 2f;
+ mIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half));
+ mIcon.setCallback(this);
+ invalidate();
+ }
+ }
+
+ public void setBackgroundColor(ColorStateList color) {
+ mBackground.setColor(color);
+ invalidate();
+ }
+
+ @Override
+ public void setBackgroundColor(int color) {
+ mBackground.setColor(color);
+ invalidate();
+ }
+
+ /**
+ * Show this button at the specific location. If this button isn't attached to any parent view yet,
+ * it will be add to activity's root view. If not, it will just update the location.
+ *
+ * @param activity The activity that this button will be attached to.
+ * @param x The x value of anchor point.
+ * @param y The y value of anchor point.
+ * @param gravity The gravity apply with this button.
+ * @see Gravity
+ */
+ public void show(Activity activity, int x, int y, int gravity) {
+ if (getParent() == null) {
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight());
+ updateParams(x, y, gravity, params);
+
+ activity.getWindow().addContentView(this, params);
+ } else
+ updateLocation(x, y, gravity);
+ }
+
+ /**
+ * Show this button at the specific location. If this button isn't attached to any parent view yet,
+ * it will be add to activity's root view. If not, it will just update the location.
+ *
+ * @param parent The parent view. Should be {@link FrameLayout} or {@link RelativeLayout}
+ * @param x The x value of anchor point.
+ * @param y The y value of anchor point.
+ * @param gravity The gravity apply with this button.
+ * @see Gravity
+ */
+ public void show(ViewGroup parent, int x, int y, int gravity) {
+ if (getParent() == null) {
+ ViewGroup.LayoutParams params = parent.generateLayoutParams(null);
+ params.width = mBackground.getIntrinsicWidth();
+ params.height = mBackground.getIntrinsicHeight();
+ updateParams(x, y, gravity, params);
+
+ parent.addView(this, params);
+ } else
+ updateLocation(x, y, gravity);
+ }
+
+ /**
+ * Update the location of this button. This method only work if it's already attached to a parent view.
+ *
+ * @param x The x value of anchor point.
+ * @param y The y value of anchor point.
+ * @param gravity The gravity apply with this button.
+ * @see Gravity
+ */
+ public void updateLocation(int x, int y, int gravity) {
+ if (getParent() != null)
+ updateParams(x, y, gravity, getLayoutParams());
+ else
+ Log.v(FloatingActionButton.class.getSimpleName(), "updateLocation() is called without parent");
+ }
+
+ private void updateParams(int x, int y, int gravity, ViewGroup.LayoutParams params) {
+ int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+
+ switch (horizontalGravity) {
+ case Gravity.START:
+ setLeftMargin(params, (int) (x - mBackground.getPaddingLeft()));
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ setLeftMargin(params, (int) (x - mBackground.getCenterX()));
+ break;
+ case Gravity.END:
+ setLeftMargin(params, (int) (x - mBackground.getPaddingLeft() - mBackground.getRadius() * 2));
+ break;
+ default:
+ setLeftMargin(params, (int) (x - mBackground.getPaddingLeft()));
+ break;
+ }
+
+ int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (verticalGravity) {
+ case Gravity.TOP:
+ setTopMargin(params, (int) (y - mBackground.getPaddingTop()));
+ break;
+ case Gravity.CENTER_VERTICAL:
+ setTopMargin(params, (int) (y - mBackground.getCenterY()));
+ break;
+ case Gravity.BOTTOM:
+ setTopMargin(params, (int) (y - mBackground.getPaddingTop() - mBackground.getRadius() * 2));
+ break;
+ default:
+ setTopMargin(params, (int) (y - mBackground.getPaddingTop()));
+ break;
+ }
+
+ setLayoutParams(params);
+ }
+
+ private void setLeftMargin(ViewGroup.LayoutParams params, int value) {
+ if (params instanceof FrameLayout.LayoutParams)
+ ((FrameLayout.LayoutParams) params).leftMargin = value;
+ else if (params instanceof RelativeLayout.LayoutParams)
+ ((RelativeLayout.LayoutParams) params).leftMargin = value;
+ else
+ Log.v(FloatingActionButton.class.getSimpleName(), "cannot recognize LayoutParams: " + params);
+ }
+
+ private void setTopMargin(ViewGroup.LayoutParams params, int value) {
+ if (params instanceof FrameLayout.LayoutParams)
+ ((FrameLayout.LayoutParams) params).topMargin = value;
+ else if (params instanceof RelativeLayout.LayoutParams)
+ ((RelativeLayout.LayoutParams) params).topMargin = value;
+ else
+ Log.v(FloatingActionButton.class.getSimpleName(), "cannot recognize LayoutParams: " + params);
+ }
+
+ /**
+ * Remove this button from parent view.
+ */
+ public void dismiss() {
+ if (getParent() != null)
+ ((ViewGroup) getParent()).removeView(this);
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || mBackground == who || mIcon == who || mPrevIcon == who;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mBackground != null)
+ mBackground.setState(getDrawableState());
+ if (mIcon != null)
+ mIcon.setState(getDrawableState());
+ if (mPrevIcon != null)
+ mPrevIcon.setState(getDrawableState());
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight());
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mBackground.setBounds(0, 0, w, h);
+
+ if (mIcon != null) {
+ float half = mIconSize / 2f;
+ mIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half));
+ }
+
+ if (mPrevIcon != null) {
+ float half = mIconSize / 2f;
+ mPrevIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half));
+ }
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ mBackground.draw(canvas);
+ super.draw(canvas);
+ if (mPrevIcon != null)
+ mPrevIcon.draw(canvas);
+ if (mIcon != null)
+ mIcon.draw(canvas);
+ }
+
+ protected RippleManager getRippleManager() {
+ if (mRippleManager == null) {
+ synchronized (RippleManager.class) {
+ if (mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN && !mBackground.isPointerOver(event.getX(), event.getY()))
+ return false;
+
+ boolean result = super.onTouchEvent(event);
+ return getRippleManager().onTouchEvent(this, event) || result;
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ ss.state = getLineMorphingState();
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+ if (ss.state >= 0)
+ setLineMorphingState(ss.state, false);
+ requestLayout();
+ }
+
+ static class SavedState extends BaseSavedState {
+ int state;
+
+ /**
+ * Constructor called from {@link Slider#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ state = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(state);
+ }
+
+ @Override
+ public String toString() {
+ return "FloatingActionButton.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " state=" + state + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ class SwitchIconAnimator implements Runnable {
+
+ boolean mRunning = false;
+ long mStartTime;
+
+ public void resetAnimation() {
+ mStartTime = SystemClock.uptimeMillis();
+ mIcon.setAlpha(0);
+ mPrevIcon.setAlpha(255);
+ }
+
+ public boolean startAnimation(Drawable icon) {
+ if (mIcon == icon)
+ return false;
+
+ mPrevIcon = mIcon;
+ mIcon = icon;
+ float half = mIconSize / 2f;
+ mIcon.setBounds((int) (mBackground.getCenterX() - half), (int) (mBackground.getCenterY() - half), (int) (mBackground.getCenterX() + half), (int) (mBackground.getCenterY() + half));
+ mIcon.setCallback(FloatingActionButton.this);
+
+ if (getHandler() != null) {
+ resetAnimation();
+ mRunning = true;
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ } else {
+ mPrevIcon.setCallback(null);
+ unscheduleDrawable(mPrevIcon);
+ mPrevIcon = null;
+ }
+
+ invalidate();
+ return true;
+ }
+
+ public void stopAnimation() {
+ mRunning = false;
+ mPrevIcon.setCallback(null);
+ unscheduleDrawable(mPrevIcon);
+ mPrevIcon = null;
+ mIcon.setAlpha(255);
+ if (getHandler() != null)
+ getHandler().removeCallbacks(this);
+ invalidate();
+ }
+
+ @Override
+ public void run() {
+ long curTime = SystemClock.uptimeMillis();
+ float progress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration);
+ float value = mInterpolator.getInterpolation(progress);
+
+ mIcon.setAlpha(Math.round(255 * value));
+ mPrevIcon.setAlpha(Math.round(255 * (1f - value)));
+
+ if (progress == 1f)
+ stopAnimation();
+
+ if (mRunning) {
+ if (getHandler() != null)
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ else
+ stopAnimation();
+ }
+
+ invalidate();
+ }
+
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/FrameLayout.java b/material/src/main/java/com/rey/material/widget/FrameLayout.java
new file mode 100644
index 0000000..f6883cb
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/FrameLayout.java
@@ -0,0 +1,126 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ViewUtil;
+
+public class FrameLayout extends android.widget.FrameLayout implements ThemeManager.OnThemeChangedListener{
+
+ private RippleManager mRippleManager;
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ public FrameLayout(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public FrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public FrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable drawable) {
+ Drawable background = getBackground();
+ if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
+ ((RippleDrawable) background).setBackgroundDrawable(drawable);
+ else
+ super.setBackgroundDrawable(drawable);
+ }
+
+ protected RippleManager getRippleManager(){
+ if(mRippleManager == null){
+ synchronized (RippleManager.class){
+ if(mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ boolean result = super.onTouchEvent(event);
+ return getRippleManager().onTouchEvent(this, event) || result;
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/ImageButton.java b/material/src/main/java/com/rey/material/widget/ImageButton.java
new file mode 100644
index 0000000..97af751
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/ImageButton.java
@@ -0,0 +1,125 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ViewUtil;
+
+public class ImageButton extends android.widget.ImageButton implements ThemeManager.OnThemeChangedListener{
+
+ private RippleManager mRippleManager;
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ public ImageButton(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public ImageButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void setBackground(Drawable drawable) {
+ Drawable background = getBackground();
+ if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
+ ((RippleDrawable) background).setBackgroundDrawable(drawable);
+ else
+ super.setBackground(drawable);
+ }
+
+ protected RippleManager getRippleManager(){
+ if(mRippleManager == null){
+ synchronized (RippleManager.class){
+ if(mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ boolean result = super.onTouchEvent(event);
+ return getRippleManager().onTouchEvent(this, event) || result;
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/LinearLayout.java b/material/src/main/java/com/rey/material/widget/LinearLayout.java
new file mode 100644
index 0000000..e1ca3a9
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/LinearLayout.java
@@ -0,0 +1,127 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ViewUtil;
+
+public class LinearLayout extends android.widget.LinearLayout implements ThemeManager.OnThemeChangedListener{
+
+ private RippleManager mRippleManager;
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ public LinearLayout(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public LinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void setBackground(Drawable drawable) {
+ Drawable background = getBackground();
+ if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
+ ((RippleDrawable) background).setBackgroundDrawable(drawable);
+ else
+ super.setBackground(drawable);
+ }
+
+ protected RippleManager getRippleManager(){
+ if(mRippleManager == null){
+ synchronized (RippleManager.class){
+ if(mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ boolean result = super.onTouchEvent(event);
+ return getRippleManager().onTouchEvent(this, event) || result;
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/ListPopupWindow.java b/material/src/main/java/com/rey/material/widget/ListPopupWindow.java
new file mode 100644
index 0000000..d5bdc2f
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/ListPopupWindow.java
@@ -0,0 +1,1823 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rey.material.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.v4.text.TextUtilsCompat;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewPropertyAnimatorCompat;
+import android.support.v4.widget.ListViewAutoScrollHelper;
+import android.support.v4.widget.PopupWindowCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+
+import com.rey.material.R;
+
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+/**
+ * This is a copy of android.support.v7.widget.ListPopupWindow.
+ * Just change DropDownListView's parent class to com.rey.material.widget.ListView to support
+ * RippleEffect in child view.
+ *
+ * @see android.widget.ListPopupWindow
+ */
+public class ListPopupWindow {
+ private static final String TAG = "ListPopupWindow";
+ private static final boolean DEBUG = false;
+
+ /**
+ * This value controls the length of time that the user
+ * must leave a pointer down without scrolling to expand
+ * the autocomplete dropdown list to cover the IME.
+ */
+ private static final int EXPAND_LIST_TIMEOUT = 250;
+
+ private static Method sClipToWindowEnabledMethod;
+
+ static {
+ try {
+ sClipToWindowEnabledMethod = android.widget.PopupWindow.class.getDeclaredMethod(
+ "setClipToScreenEnabled", boolean.class);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Could not find method setClipToScreenEnabled() on PopupWindow. Oh well.");
+ }
+ }
+
+ private Context mContext;
+ private PopupWindow mPopup;
+ private ListAdapter mAdapter;
+ private DropDownListView mDropDownList;
+
+ private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
+ private int mDropDownHorizontalOffset;
+ private int mDropDownVerticalOffset;
+ private boolean mDropDownVerticalOffsetSet;
+
+ private int mItemAnimationId;
+ private int mItemAnimationOffset;
+
+ private int mDropDownGravity = Gravity.NO_GRAVITY;
+
+ private boolean mDropDownAlwaysVisible = false;
+ private boolean mForceIgnoreOutsideTouch = false;
+ int mListItemExpandMaximum = Integer.MAX_VALUE;
+
+ private View mPromptView;
+ private int mPromptPosition = POSITION_PROMPT_ABOVE;
+
+ private DataSetObserver mObserver;
+
+ private View mDropDownAnchorView;
+
+ private Drawable mDropDownListHighlight;
+
+ private AdapterView.OnItemClickListener mItemClickListener;
+ private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+ private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
+ private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
+ private final PopupScrollListener mScrollListener = new PopupScrollListener();
+ private final ListSelectorHider mHideSelector = new ListSelectorHider();
+ private Runnable mShowDropDownRunnable;
+
+ private Handler mHandler = new Handler();
+
+ private Rect mTempRect = new Rect();
+
+ private boolean mModal;
+
+ private int mLayoutDirection;
+
+ /**
+ * The provided prompt view should appear above list content.
+ *
+ * @see #setPromptPosition(int)
+ * @see #getPromptPosition()
+ * @see #setPromptView(View)
+ */
+ public static final int POSITION_PROMPT_ABOVE = 0;
+
+ /**
+ * The provided prompt view should appear below list content.
+ *
+ * @see #setPromptPosition(int)
+ * @see #getPromptPosition()
+ * @see #setPromptView(View)
+ */
+ public static final int POSITION_PROMPT_BELOW = 1;
+
+ /**
+ * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
+ * If used to specify a popup width, the popup will match the width of the anchor view.
+ * If used to specify a popup height, the popup will fill available space.
+ */
+ public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
+
+ /**
+ * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
+ * If used to specify a popup width, the popup will use the width of its content.
+ */
+ public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: the requirements for the
+ * input method should be based on the focusability of the popup. That is
+ * if it is focusable than it needs to work with the input method, else
+ * it doesn't.
+ */
+ public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
+ * work with an input method, regardless of whether it is focusable. This
+ * means that it will always be displayed so that the user can also operate
+ * the input method while it is shown.
+ */
+ public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
+
+ /**
+ * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
+ * work with an input method, regardless of whether it is focusable. This
+ * means that it will always be displayed to use as much space on the
+ * screen as needed, regardless of whether this covers the input method.
+ */
+ public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ */
+ public ListPopupWindow(Context context) {
+ this(context, null, R.attr.listPopupWindowStyle, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ */
+ public ListPopupWindow(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.listPopupWindowStyle, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ * @param defStyleAttr Default style attribute to use for popup content.
+ */
+ public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr){
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Create a new, empty popup window capable of displaying items from a ListAdapter.
+ * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+ *
+ * @param context Context used for contained views.
+ * @param attrs Attributes from inflating parent views used to style the popup.
+ * @param defStyleAttr Default style attribute to use for popup content.
+ * @param defStyleRes Default style to use for popup content.
+ */
+ public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ mContext = context;
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,
+ defStyleAttr, defStyleRes);
+ mDropDownHorizontalOffset = a.getDimensionPixelOffset(
+ R.styleable.ListPopupWindow_android_dropDownHorizontalOffset, 0);
+ mDropDownVerticalOffset = a.getDimensionPixelOffset(
+ R.styleable.ListPopupWindow_android_dropDownVerticalOffset, 0);
+ if (mDropDownVerticalOffset != 0) {
+ mDropDownVerticalOffsetSet = true;
+ }
+ a.recycle();
+
+ mPopup = new PopupWindow(context, attrs, defStyleAttr);
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+
+ // Set the default layout direction to match the default locale one
+ final Locale locale = mContext.getResources().getConfiguration().locale;
+ mLayoutDirection = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
+ }
+
+ public void setItemAnimation(int id){
+ mItemAnimationId = id;
+ }
+
+ public void setItemAnimationOffset(int offset){
+ mItemAnimationOffset = offset;
+ }
+
+ public void setBackgroundDrawable(Drawable background){
+ mPopup.setBackgroundDrawable(background);
+ }
+
+ public Drawable getBackground(){
+ return mPopup.getBackground();
+ }
+
+ /**
+ * Sets the adapter that provides the data and the views to represent the data
+ * in this popup window.
+ *
+ * @param adapter The adapter to use to create this window's content.
+ */
+ public void setAdapter(ListAdapter adapter) {
+ if (mObserver == null) {
+ mObserver = new PopupDataSetObserver();
+ } else if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ mAdapter = adapter;
+ if (mAdapter != null) {
+ adapter.registerDataSetObserver(mObserver);
+ }
+
+ if (mDropDownList != null) {
+ mDropDownList.setAdapter(mAdapter);
+ }
+ }
+
+ /**
+ * Set where the optional prompt view should appear. The default is
+ * {@link #POSITION_PROMPT_ABOVE}.
+ *
+ * @param position A position constant declaring where the prompt should be displayed.
+ *
+ * @see #POSITION_PROMPT_ABOVE
+ * @see #POSITION_PROMPT_BELOW
+ */
+ public void setPromptPosition(int position) {
+ mPromptPosition = position;
+ }
+
+ /**
+ * @return Where the optional prompt view should appear.
+ *
+ * @see #POSITION_PROMPT_ABOVE
+ * @see #POSITION_PROMPT_BELOW
+ */
+ public int getPromptPosition() {
+ return mPromptPosition;
+ }
+
+ /**
+ * Set whether this window should be modal when shown.
+ *
+ * If a popup window is modal, it will receive all touch and key input.
+ * If the user touches outside the popup window's content area the popup window
+ * will be dismissed.
+ *
+ * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
+ */
+ public void setModal(boolean modal) {
+ mModal = modal;
+ mPopup.setFocusable(modal);
+ }
+
+ /**
+ * Returns whether the popup window will be modal when shown.
+ *
+ * @return {@code true} if the popup window will be modal, {@code false} otherwise.
+ */
+ public boolean isModal() {
+ return mModal;
+ }
+
+ /**
+ * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
+ * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
+ * ignore outside touch even when the drop down is not set to always visible.
+ *
+ * @hide Used only by AutoCompleteTextView to handle some internal special cases.
+ */
+ public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
+ mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
+ }
+
+ /**
+ * Sets whether the drop-down should remain visible under certain conditions.
+ *
+ * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
+ * of the size or content of the list. {@link #getBackground()} will fill any space
+ * that is not used by the list.
+ *
+ * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+ *
+ * @hide Only used by AutoCompleteTextView under special conditions.
+ */
+ public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+ mDropDownAlwaysVisible = dropDownAlwaysVisible;
+ }
+
+ /**
+ * @return Whether the drop-down is visible under special conditions.
+ *
+ * @hide Only used by AutoCompleteTextView under special conditions.
+ */
+ public boolean isDropDownAlwaysVisible() {
+ return mDropDownAlwaysVisible;
+ }
+
+ /**
+ * Sets the operating mode for the soft input area.
+ *
+ * @param mode The desired mode, see
+ * {@link android.view.WindowManager.LayoutParams#softInputMode}
+ * for the full list
+ *
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ * @see #getSoftInputMode()
+ */
+ public void setSoftInputMode(int mode) {
+ mPopup.setSoftInputMode(mode);
+ }
+
+ /**
+ * Returns the current value in {@link #setSoftInputMode(int)}.
+ *
+ * @see #setSoftInputMode(int)
+ * @see android.view.WindowManager.LayoutParams#softInputMode
+ */
+ public int getSoftInputMode() {
+ return mPopup.getSoftInputMode();
+ }
+
+ /**
+ * Sets a drawable to use as the list item selector.
+ *
+ * @param selector List selector drawable to use in the popup.
+ */
+ public void setListSelector(Drawable selector) {
+ mDropDownListHighlight = selector;
+ }
+
+ /**
+ * Set an animation style to use when the popup window is shown or dismissed.
+ *
+ * @param animationStyle Animation style to use.
+ */
+ public void setAnimationStyle(int animationStyle) {
+ mPopup.setAnimationStyle(animationStyle);
+ }
+
+ /**
+ * Returns the animation style that will be used when the popup window is shown or dismissed.
+ *
+ * @return Animation style that will be used.
+ */
+ public int getAnimationStyle() {
+ return mPopup.getAnimationStyle();
+ }
+
+ /**
+ * Returns the view that will be used to anchor this popup.
+ *
+ * @return The popup's anchor view
+ */
+ public View getAnchorView() {
+ return mDropDownAnchorView;
+ }
+
+ /**
+ * Sets the popup's anchor view. This popup will always be positioned relative to the anchor
+ * view when shown.
+ *
+ * @param anchor The view to use as an anchor.
+ */
+ public void setAnchorView(View anchor) {
+ mDropDownAnchorView = anchor;
+ }
+
+ /**
+ * @return The horizontal offset of the popup from its anchor in pixels.
+ */
+ public int getHorizontalOffset() {
+ return mDropDownHorizontalOffset;
+ }
+
+ /**
+ * Set the horizontal offset of this popup from its anchor view in pixels.
+ *
+ * @param offset The horizontal offset of the popup from its anchor.
+ */
+ public void setHorizontalOffset(int offset) {
+ mDropDownHorizontalOffset = offset;
+ }
+
+ /**
+ * @return The vertical offset of the popup from its anchor in pixels.
+ */
+ public int getVerticalOffset() {
+ if (!mDropDownVerticalOffsetSet) {
+ return 0;
+ }
+ return mDropDownVerticalOffset;
+ }
+
+ /**
+ * Set the vertical offset of this popup from its anchor view in pixels.
+ *
+ * @param offset The vertical offset of the popup from its anchor.
+ */
+ public void setVerticalOffset(int offset) {
+ mDropDownVerticalOffset = offset;
+ mDropDownVerticalOffsetSet = true;
+ }
+
+ /**
+ * Set the gravity of the dropdown list. This is commonly used to
+ * set gravity to START or END for alignment with the anchor.
+ *
+ * @param gravity Gravity value to use
+ */
+ public void setDropDownGravity(int gravity) {
+ mDropDownGravity = gravity;
+ }
+
+ /**
+ * @return The width of the popup window in pixels.
+ */
+ public int getWidth() {
+ return mDropDownWidth;
+ }
+
+ /**
+ * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
+ * or {@link #WRAP_CONTENT}.
+ *
+ * @param width Width of the popup window.
+ */
+ public void setWidth(int width) {
+ mDropDownWidth = width;
+ }
+
+ /**
+ * Sets the width of the popup window by the size of its content. The final width may be
+ * larger to accommodate styled window dressing.
+ *
+ * @param width Desired width of content in pixels.
+ */
+ public void setContentWidth(int width) {
+ Drawable popupBackground = mPopup.getBackground();
+ if (popupBackground != null) {
+ popupBackground.getPadding(mTempRect);
+ mDropDownWidth = mTempRect.left + mTempRect.right + width;
+ } else {
+ setWidth(width);
+ }
+ }
+
+ /**
+ * @return The height of the popup window in pixels.
+ */
+ public int getHeight() {
+ return mDropDownHeight;
+ }
+
+ /**
+ * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
+ *
+ * @param height Height of the popup window.
+ */
+ public void setHeight(int height) {
+ mDropDownHeight = height;
+ }
+
+ /**
+ * Sets a listener to receive events when a list item is clicked.
+ *
+ * @param clickListener Listener to register
+ *
+ * @see ListView#setOnItemClickListener(AdapterView.OnItemClickListener)
+ */
+ public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) {
+ mItemClickListener = clickListener;
+ }
+
+ /**
+ * Sets a listener to receive events when a list item is selected.
+ *
+ * @param selectedListener Listener to register.
+ *
+ * @see ListView#setOnItemSelectedListener(AdapterView.OnItemSelectedListener)
+ */
+ public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) {
+ mItemSelectedListener = selectedListener;
+ }
+
+ /**
+ * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
+ * is controlled by {@link #setPromptPosition(int)}.
+ *
+ * @param prompt View to use as an informational prompt.
+ */
+ public void setPromptView(View prompt) {
+ boolean showing = isShowing();
+ if (showing) {
+ removePromptView();
+ }
+ mPromptView = prompt;
+ if (showing) {
+ show();
+ }
+ }
+
+ /**
+ * Post a {@link #show()} call to the UI thread.
+ */
+ public void postShow() {
+ mHandler.post(mShowDropDownRunnable);
+ }
+
+ /**
+ * Show the popup list. If the list is already showing, this method
+ * will recalculate the popup's size and position.
+ */
+ public void show() {
+ int height = buildDropDown();
+
+ int widthSpec = 0;
+ int heightSpec = 0;
+
+ boolean noInputMethod = isInputMethodNotNeeded();
+
+ if (mPopup.isShowing()) {
+ if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+ // The call to PopupWindow's update method below can accept -1 for any
+ // value you do not want to update.
+ widthSpec = -1;
+ } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ widthSpec = getAnchorView().getWidth();
+ } else {
+ widthSpec = mDropDownWidth;
+ }
+
+ if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ // The call to PopupWindow's update method below can accept -1 for any
+ // value you do not want to update.
+ heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
+ if (noInputMethod) {
+ mPopup.setWindowLayoutMode(
+ mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+ ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
+ } else {
+ mPopup.setWindowLayoutMode(
+ mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+ ViewGroup.LayoutParams.MATCH_PARENT : 0,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+ } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ heightSpec = height;
+ } else {
+ heightSpec = mDropDownHeight;
+ }
+
+ mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+
+ mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
+ mDropDownVerticalOffset, widthSpec, heightSpec);
+ } else {
+ if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+ widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+ } else {
+ if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mPopup.setWidth(getAnchorView().getWidth());
+ } else {
+ mPopup.setWidth(mDropDownWidth);
+ }
+ }
+
+ if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+ } else {
+ if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mPopup.setHeight(height);
+ } else {
+ mPopup.setHeight(mDropDownHeight);
+ }
+ }
+
+ mPopup.setWindowLayoutMode(widthSpec, heightSpec);
+ setPopupClipToScreenEnabled(true);
+
+ // use outside touchable to dismiss drop down when touching outside of it, so
+ // only set this if the dropdown is not always visible
+ mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+ mPopup.setTouchInterceptor(mTouchInterceptor);
+ PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mDropDownHorizontalOffset,
+ mDropDownVerticalOffset, mDropDownGravity);
+ mDropDownList.setSelection(ListView.INVALID_POSITION);
+
+ if (!mModal || mDropDownList.isInTouchMode()) {
+ clearListSelection();
+ }
+ if (!mModal) {
+ mHandler.post(mHideSelector);
+ }
+
+ // show item animation
+ if(mItemAnimationId != 0)
+ mPopup.getContentView().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+
+ @Override
+ public boolean onPreDraw() {
+ mPopup.getContentView().getViewTreeObserver().removeOnPreDrawListener(this);
+ for(int i = 0, count = mDropDownList.getChildCount(); i < count; i ++){
+ View v = mDropDownList.getChildAt(i);
+
+ Animation anim = AnimationUtils.loadAnimation(mContext, mItemAnimationId);
+ anim.setStartOffset(mItemAnimationOffset * i);
+ v.startAnimation(anim);
+ }
+ return false;
+ }
+
+ });
+ }
+ }
+
+ /**
+ * Dismiss the popup window.
+ */
+ public void dismiss() {
+ mPopup.dismiss();
+ removePromptView();
+ mPopup.setContentView(null);
+ mDropDownList = null;
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ }
+
+ /**
+ * Set a listener to receive a callback when the popup is dismissed.
+ *
+ * @param listener Listener that will be notified when the popup is dismissed.
+ */
+ public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
+ mPopup.setOnDismissListener(listener);
+ }
+
+ private void removePromptView() {
+ if (mPromptView != null) {
+ final ViewParent parent = mPromptView.getParent();
+ if (parent instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) parent;
+ group.removeView(mPromptView);
+ }
+ }
+ }
+
+ /**
+ * Control how the popup operates with an input method: one of
+ * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
+ * or {@link #INPUT_METHOD_NOT_NEEDED}.
+ *
+ *
If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown or through a manual call to the {@link #show()}
+ * method.
+ *
+ * @see #getInputMethodMode()
+ * @see #show()
+ */
+ public void setInputMethodMode(int mode) {
+ mPopup.setInputMethodMode(mode);
+ }
+
+ /**
+ * Return the current value in {@link #setInputMethodMode(int)}.
+ *
+ * @see #setInputMethodMode(int)
+ */
+ public int getInputMethodMode() {
+ return mPopup.getInputMethodMode();
+ }
+
+ /**
+ * Set the selected position of the list.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ *
+ * @param position List position to set as selected.
+ */
+ public void setSelection(int position) {
+ DropDownListView list = mDropDownList;
+ if (isShowing() && list != null) {
+ list.mListSelectionHidden = false;
+ list.setSelection(position);
+
+ if (Build.VERSION.SDK_INT >= 11) {
+ if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
+ list.setItemChecked(position, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear any current list selection.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ */
+ public void clearListSelection() {
+ final DropDownListView list = mDropDownList;
+ if (list != null) {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ list.mListSelectionHidden = true;
+ //list.hideSelector();
+ list.requestLayout();
+ }
+ }
+
+ /**
+ * @return {@code true} if the popup is currently showing, {@code false} otherwise.
+ */
+ public boolean isShowing() {
+ return mPopup.isShowing();
+ }
+
+ /**
+ * @return {@code true} if this popup is configured to assume the user does not need
+ * to interact with the IME while it is showing, {@code false} otherwise.
+ */
+ public boolean isInputMethodNotNeeded() {
+ return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
+ }
+
+ /**
+ * Perform an item click operation on the specified list adapter position.
+ *
+ * @param position Adapter position for performing the click
+ * @return true if the click action could be performed, false if not.
+ * (e.g. if the popup was not showing, this method would return false.)
+ */
+ public boolean performItemClick(int position) {
+ if (isShowing()) {
+ if (mItemClickListener != null) {
+ final DropDownListView list = mDropDownList;
+ final View child = list.getChildAt(position - list.getFirstVisiblePosition());
+ final ListAdapter adapter = list.getAdapter();
+ mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return The currently selected item or null if the popup is not showing.
+ */
+ public Object getSelectedItem() {
+ if (!isShowing()) {
+ return null;
+ }
+ return mDropDownList.getSelectedItem();
+ }
+
+ /**
+ * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
+ * if {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedItemPosition()
+ */
+ public int getSelectedItemPosition() {
+ if (!isShowing()) {
+ return ListView.INVALID_POSITION;
+ }
+ return mDropDownList.getSelectedItemPosition();
+ }
+
+ /**
+ * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
+ * if {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedItemId()
+ */
+ public long getSelectedItemId() {
+ if (!isShowing()) {
+ return ListView.INVALID_ROW_ID;
+ }
+ return mDropDownList.getSelectedItemId();
+ }
+
+ /**
+ * @return The View for the currently selected item or null if
+ * {@link #isShowing()} == {@code false}.
+ *
+ * @see ListView#getSelectedView()
+ */
+ public View getSelectedView() {
+ if (!isShowing()) {
+ return null;
+ }
+ return mDropDownList.getSelectedView();
+ }
+
+ /**
+ * @return The {@link ListView} displayed within the popup window.
+ * Only valid when {@link #isShowing()} == {@code true}.
+ */
+ public ListView getListView() {
+ return mDropDownList;
+ }
+
+ public PopupWindow getPopup(){
+ return mPopup;
+ }
+
+ /**
+ * The maximum number of list items that can be visible and still have
+ * the list expand when touched.
+ *
+ * @param max Max number of items that can be visible and still allow the list to expand.
+ */
+ void setListItemExpandMax(int max) {
+ mListItemExpandMaximum = max;
+ }
+
+ /**
+ * Filter key down events. By forwarding key down events to this function,
+ * views using non-modal ListPopupWindow can have it handle key selection of items.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyDown
+ * @param event event param passed to the host view's onKeyDown
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // when the drop down is shown, we drive it directly
+ if (isShowing()) {
+ // the key events are forwarded to the list in the drop down view
+ // note that ListView handles space but we don't want that to happen
+ // also if selection is not currently in the drop down, then don't
+ // let center or enter presses go there since that would cause it
+ // to select one of its items
+ if (keyCode != KeyEvent.KEYCODE_SPACE
+ && (mDropDownList.getSelectedItemPosition() >= 0
+ || !isConfirmKey(keyCode))) {
+ int curIndex = mDropDownList.getSelectedItemPosition();
+ boolean consumed;
+
+ final boolean below = !mPopup.isAboveAnchor();
+
+ final ListAdapter adapter = mAdapter;
+
+ boolean allEnabled;
+ int firstItem = Integer.MAX_VALUE;
+ int lastItem = Integer.MIN_VALUE;
+
+ if (adapter != null) {
+ allEnabled = adapter.areAllItemsEnabled();
+ firstItem = allEnabled ? 0 :
+ mDropDownList.lookForSelectablePosition(0, true);
+ lastItem = allEnabled ? adapter.getCount() - 1 :
+ mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
+ }
+
+ if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
+ (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
+ // When the selection is at the top, we block the key
+ // event to prevent focus from moving.
+ clearListSelection();
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ show();
+ return true;
+ } else {
+ // WARNING: Please read the comment where mListSelectionHidden
+ // is declared
+ mDropDownList.mListSelectionHidden = false;
+ }
+
+ consumed = mDropDownList.onKeyDown(keyCode, event);
+ if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
+
+ if (consumed) {
+ // If it handled the key event, then the user is
+ // navigating in the list, so we should put it in front.
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ // Here's a little trick we need to do to make sure that
+ // the list view is actually showing its focus indicator,
+ // by ensuring it has focus and getting its window out
+ // of touch mode.
+ mDropDownList.requestFocusFromTouch();
+ show();
+
+ switch (keyCode) {
+ // avoid passing the focus from the text view to the
+ // next component
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ return true;
+ }
+ } else {
+ if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+ // when the selection is at the bottom, we block the
+ // event to avoid going to the next focusable widget
+ if (curIndex == lastItem) {
+ return true;
+ }
+ } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
+ curIndex == firstItem) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Filter key down events. By forwarding key up events to this function,
+ * views using non-modal ListPopupWindow can have it handle key selection of items.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyUp
+ * @param event event param passed to the host view's onKeyUp
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
+ boolean consumed = mDropDownList.onKeyUp(keyCode, event);
+ if (consumed && isConfirmKey(keyCode)) {
+ // if the list accepts the key events and the key event was a click, the text view
+ // gets the selected item from the drop down as its content
+ dismiss();
+ }
+ return consumed;
+ }
+ return false;
+ }
+
+ /**
+ * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
+ * events to this function, views using ListPopupWindow can have it dismiss the popup
+ * when the back key is pressed.
+ *
+ * @param keyCode keyCode param passed to the host view's onKeyPreIme
+ * @param event event param passed to the host view's onKeyPreIme
+ * @return true if the event was handled, false if it was ignored.
+ *
+ * @see #setModal(boolean)
+ */
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
+ // special case for the back key, we do not even try to send it
+ // to the drop down list but instead, consume it immediately
+ final View anchorView = mDropDownAnchorView;
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+ KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
+ if (state != null) {
+ state.startTracking(event, this);
+ }
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
+ if (state != null) {
+ state.handleUpEvent(event);
+ }
+ if (event.isTracking() && !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns an {@link OnTouchListener} that can be added to the source view
+ * to implement drag-to-open behavior. Generally, the source view should be
+ * the same view that was passed to {@link #setAnchorView}.
+ *
+ * When the listener is set on a view, touching that view and dragging
+ * outside of its bounds will open the popup window. Lifting will select the
+ * currently touched list item.
+ *
+ * Example usage:
+ *
+ * ListPopupWindow myPopup = new ListPopupWindow(context);
+ * myPopup.setAnchor(myAnchor);
+ * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
+ * myAnchor.setOnTouchListener(dragListener);
+ *
+ *
+ * @param src the view on which the resulting listener will be set
+ * @return a touch listener that controls drag-to-open behavior
+ */
+ public OnTouchListener createDragToOpenListener(View src) {
+ return new ForwardingListener(src) {
+ @Override
+ public ListPopupWindow getPopup() {
+ return ListPopupWindow.this;
+ }
+ };
+ }
+
+ private int getSystemBarHeight(String resourceName) {
+ int height = 0;
+ int resourceId = mContext.getResources().getIdentifier(resourceName, "dimen", "android");
+ if (resourceId > 0) {
+ height = mContext.getResources().getDimensionPixelSize(resourceId);
+ }
+ return height;
+ }
+
+ /**
+ * Builds the popup window's content and returns the height the popup
+ * should have. Returns -1 when the content already exists.
+ *
+ * @return the content's height or -1 if content already exists
+ */
+ private int buildDropDown() {
+ int otherHeights = 0;
+
+ if (mDropDownList == null) {
+ ViewGroup dropDownView;
+ Context context = mContext;
+
+ /**
+ * This Runnable exists for the sole purpose of checking if the view layout has got
+ * completed and if so call showDropDown to display the drop down. This is used to show
+ * the drop down as soon as possible after user opens up the search dialog, without
+ * waiting for the normal UI pipeline to do it's job which is slower than this method.
+ */
+ mShowDropDownRunnable = new Runnable() {
+ public void run() {
+ // View layout should be all done before displaying the drop down.
+ View view = getAnchorView();
+ if (view != null && view.getWindowToken() != null) {
+ show();
+ }
+ }
+ };
+
+ mDropDownList = new DropDownListView(context, !mModal);
+ if (mDropDownListHighlight != null) {
+ mDropDownList.setSelector(mDropDownListHighlight);
+ }
+ mDropDownList.setAdapter(mAdapter);
+ mDropDownList.setOnItemClickListener(mItemClickListener);
+ mDropDownList.setFocusable(true);
+ mDropDownList.setFocusableInTouchMode(true);
+ mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView> parent, View view,
+ int position, long id) {
+
+ if (position != -1) {
+ DropDownListView dropDownList = mDropDownList;
+
+ if (dropDownList != null) {
+ dropDownList.mListSelectionHidden = false;
+ }
+ }
+ }
+
+ public void onNothingSelected(AdapterView> parent) {
+ }
+ });
+ mDropDownList.setOnScrollListener(mScrollListener);
+
+ if (mItemSelectedListener != null) {
+ mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
+ }
+
+ dropDownView = mDropDownList;
+
+ View hintView = mPromptView;
+ if (hintView != null) {
+ // if a hint has been specified, we accomodate more space for it and
+ // add a text view in the drop down menu, at the bottom of the list
+ LinearLayout hintContainer = new LinearLayout(context);
+ hintContainer.setOrientation(LinearLayout.VERTICAL);
+
+ LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
+ );
+
+ switch (mPromptPosition) {
+ case POSITION_PROMPT_BELOW:
+ hintContainer.addView(dropDownView, hintParams);
+ hintContainer.addView(hintView);
+ break;
+
+ case POSITION_PROMPT_ABOVE:
+ hintContainer.addView(hintView);
+ hintContainer.addView(dropDownView, hintParams);
+ break;
+
+ default:
+ Log.e(TAG, "Invalid hint position " + mPromptPosition);
+ break;
+ }
+
+ // measure the hint's height to find how much more vertical space
+ // we need to add to the drop down's height
+ int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST);
+ int heightSpec = MeasureSpec.UNSPECIFIED;
+ hintView.measure(widthSpec, heightSpec);
+
+ hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
+ otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+
+ dropDownView = hintContainer;
+ }
+
+ mPopup.setContentView(dropDownView);
+
+ } else {
+ final View view = mPromptView;
+ if (view != null) {
+ LinearLayout.LayoutParams hintParams =
+ (LinearLayout.LayoutParams) view.getLayoutParams();
+ otherHeights = view.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+ }
+ }
+
+ // getMaxAvailableHeight() subtracts the padding, so we put it back
+ // to get the available height for the whole window
+ int padding = 0;
+ Drawable background = mPopup.getBackground();
+ if (background != null) {
+ background.getPadding(mTempRect);
+ padding = mTempRect.top + mTempRect.bottom;
+
+ // If we don't have an explicit vertical offset, determine one from the window
+ // background so that content will line up.
+ if (!mDropDownVerticalOffsetSet) {
+ mDropDownVerticalOffset = -mTempRect.top;
+ }
+ } else {
+ mTempRect.setEmpty();
+ }
+
+ int systemBarsReservedSpace = 0;
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ // getMaxAvailableHeight() on Lollipop seems to ignore the system bars.
+ systemBarsReservedSpace = Math.max(
+ getSystemBarHeight("status_bar_height"),
+ getSystemBarHeight("navigation_bar_height")
+ );
+ }
+
+ // Max height available on the screen for a popup.
+ boolean ignoreBottomDecorations =
+ mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
+ final int maxHeight = mPopup.getMaxAvailableHeight(
+ getAnchorView(), mDropDownVerticalOffset /*, ignoreBottomDecorations*/)
+ - systemBarsReservedSpace;
+
+ if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+ return maxHeight + padding;
+ }
+
+ final int childWidthSpec;
+ switch (mDropDownWidth) {
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ childWidthSpec = MeasureSpec.makeMeasureSpec(
+ mContext.getResources().getDisplayMetrics().widthPixels -
+ (mTempRect.left + mTempRect.right),
+ MeasureSpec.AT_MOST);
+ break;
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ childWidthSpec = MeasureSpec.makeMeasureSpec(
+ mContext.getResources().getDisplayMetrics().widthPixels -
+ (mTempRect.left + mTempRect.right),
+ MeasureSpec.EXACTLY);
+ break;
+ default:
+ childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
+ break;
+ }
+
+ final int listContent = mDropDownList.measureHeightOfChildrenCompat(childWidthSpec,
+ 0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1);
+ // add padding only if the list has items in it, that way we don't show
+ // the popup if it is not needed
+ if (listContent > 0) otherHeights += padding;
+
+ return listContent + otherHeights;
+ }
+
+ /**
+ * Abstract class that forwards touch events to a {@link ListPopupWindow}.
+ *
+ * @hide
+ */
+ public static abstract class ForwardingListener implements OnTouchListener {
+ /** Scaled touch slop, used for detecting movement outside bounds. */
+ private final float mScaledTouchSlop;
+
+ /** Timeout before disallowing intercept on the source's parent. */
+ private final int mTapTimeout;
+ /** Timeout before accepting a long-press to start forwarding. */
+ private final int mLongPressTimeout;
+
+ /** Source view from which events are forwarded. */
+ private final View mSrc;
+
+ /** Runnable used to prevent conflicts with scrolling parents. */
+ private Runnable mDisallowIntercept;
+ /** Runnable used to trigger forwarding on long-press. */
+ private Runnable mTriggerLongPress;
+
+ /** Whether this listener is currently forwarding touch events. */
+ private boolean mForwarding;
+ /**
+ * Whether forwarding was initiated by a long-press. If so, we won't
+ * force the window to dismiss when the touch stream ends.
+ */
+ private boolean mWasLongPress;
+
+ /** The id of the first pointer down in the current event stream. */
+ private int mActivePointerId;
+
+ /**
+ * Temporary Matrix instance
+ */
+ private final int[] mTmpLocation = new int[2];
+
+ public ForwardingListener(View src) {
+ mSrc = src;
+ mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
+ mTapTimeout = ViewConfiguration.getTapTimeout();
+ // Use a medium-press timeout. Halfway between tap and long-press.
+ mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;
+ }
+
+ /**
+ * Returns the popup to which this listener is forwarding events.
+ *
+ * Override this to return the correct popup. If the popup is displayed
+ * asynchronously, you may also need to override
+ * {@link #onForwardingStopped} to prevent premature cancelation of
+ * forwarding.
+ *
+ * @return the popup to which this listener is forwarding events
+ */
+ public abstract ListPopupWindow getPopup();
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ final boolean wasForwarding = mForwarding;
+ final boolean forwarding;
+ if (wasForwarding) {
+ if (mWasLongPress) {
+ // If we started forwarding as a result of a long-press,
+ // just silently stop forwarding events so that the window
+ // stays open.
+ forwarding = onTouchForwarded(event);
+ } else {
+ forwarding = onTouchForwarded(event) || !onForwardingStopped();
+ }
+ } else {
+ forwarding = onTouchObserved(event) && onForwardingStarted();
+
+ if (forwarding) {
+ // Make sure we cancel any ongoing source event stream.
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
+ 0.0f, 0.0f, 0);
+ mSrc.onTouchEvent(e);
+ e.recycle();
+ }
+ }
+
+ mForwarding = forwarding;
+ return forwarding || wasForwarding;
+ }
+
+ /**
+ * Called when forwarding would like to start.
By default, this will show the popup
+ * returned by {@link #getPopup()}. It may be overridden to perform another action, like
+ * clicking the source view or preparing the popup before showing it.
+ *
+ * @return true to start forwarding, false otherwise
+ */
+ protected boolean onForwardingStarted() {
+ final ListPopupWindow popup = getPopup();
+ if (popup != null && !popup.isShowing()) {
+ popup.show();
+ }
+ return true;
+ }
+
+ /**
+ * Called when forwarding would like to stop.
By default, this will dismiss the popup
+ * returned by {@link #getPopup()}. It may be overridden to perform some other action.
+ *
+ * @return true to stop forwarding, false otherwise
+ */
+ protected boolean onForwardingStopped() {
+ final ListPopupWindow popup = getPopup();
+ if (popup != null && popup.isShowing()) {
+ popup.dismiss();
+ }
+ return true;
+ }
+
+ /**
+ * Observes motion events and determines when to start forwarding.
+ *
+ * @param srcEvent motion event in source view coordinates
+ * @return true to start forwarding motion events, false otherwise
+ */
+ private boolean onTouchObserved(MotionEvent srcEvent) {
+ final View src = mSrc;
+ if (!src.isEnabled()) {
+ return false;
+ }
+
+ final int actionMasked = MotionEventCompat.getActionMasked(srcEvent);
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ mActivePointerId = srcEvent.getPointerId(0);
+ mWasLongPress = false;
+
+ if (mDisallowIntercept == null) {
+ mDisallowIntercept = new DisallowIntercept();
+ }
+ src.postDelayed(mDisallowIntercept, mTapTimeout);
+ if (mTriggerLongPress == null) {
+ mTriggerLongPress = new TriggerLongPress();
+ }
+ src.postDelayed(mTriggerLongPress, mLongPressTimeout);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
+ if (activePointerIndex >= 0) {
+ final float x = srcEvent.getX(activePointerIndex);
+ final float y = srcEvent.getY(activePointerIndex);
+ if (!pointInView(src, x, y, mScaledTouchSlop)) {
+ clearCallbacks();
+
+ // Don't let the parent intercept our events.
+ src.getParent().requestDisallowInterceptTouchEvent(true);
+ return true;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ clearCallbacks();
+ break;
+ }
+
+ return false;
+ }
+
+ private void clearCallbacks() {
+ if (mTriggerLongPress != null) {
+ mSrc.removeCallbacks(mTriggerLongPress);
+ }
+
+ if (mDisallowIntercept != null) {
+ mSrc.removeCallbacks(mDisallowIntercept);
+ }
+ }
+
+ private void onLongPress() {
+ clearCallbacks();
+
+ final View src = mSrc;
+ if (!src.isEnabled()) {
+ return;
+ }
+
+ if (!onForwardingStarted()) {
+ return;
+ }
+
+ // Don't let the parent intercept our events.
+ mSrc.getParent().requestDisallowInterceptTouchEvent(true);
+
+ // Make sure we cancel any ongoing source event stream.
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mSrc.onTouchEvent(e);
+ e.recycle();
+
+ mForwarding = true;
+ mWasLongPress = true;
+ }
+
+ /**
+ * Handled forwarded motion events and determines when to stop forwarding.
+ *
+ * @param srcEvent motion event in source view coordinates
+ * @return true to continue forwarding motion events, false to cancel
+ */
+ private boolean onTouchForwarded(MotionEvent srcEvent) {
+ final View src = mSrc;
+ final ListPopupWindow popup = getPopup();
+ if (popup == null || !popup.isShowing()) {
+ return false;
+ }
+
+ final DropDownListView dst = popup.mDropDownList;
+ if (dst == null || !dst.isShown()) {
+ return false;
+ }
+
+ // Convert event to destination-local coordinates.
+ final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
+ toGlobalMotionEvent(src, dstEvent);
+ toLocalMotionEvent(dst, dstEvent);
+
+ // Forward converted event to destination view, then recycle it.
+ final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
+ dstEvent.recycle();
+
+ // Always cancel forwarding when the touch stream ends.
+ final int action = MotionEventCompat.getActionMasked(srcEvent);
+ final boolean keepForwarding = action != MotionEvent.ACTION_UP
+ && action != MotionEvent.ACTION_CANCEL;
+
+ return handled && keepForwarding;
+ }
+
+ private static boolean pointInView(View view, float localX, float localY, float slop) {
+ return localX >= -slop && localY >= -slop &&
+ localX < ((view.getRight() - view.getLeft()) + slop) &&
+ localY < ((view.getBottom() - view.getTop()) + slop);
+ }
+
+ /**
+ * Emulates View.toLocalMotionEvent(). This implementation does not handle transformations
+ * (scaleX, scaleY, etc).
+ */
+ private boolean toLocalMotionEvent(View view, MotionEvent event) {
+ final int[] loc = mTmpLocation;
+ view.getLocationOnScreen(loc);
+ event.offsetLocation(-loc[0], -loc[1]);
+ return true;
+ }
+
+ /**
+ * Emulates View.toGlobalMotionEvent(). This implementation does not handle transformations
+ * (scaleX, scaleY, etc).
+ */
+ private boolean toGlobalMotionEvent(View view, MotionEvent event) {
+ final int[] loc = mTmpLocation;
+ view.getLocationOnScreen(loc);
+ event.offsetLocation(loc[0], loc[1]);
+ return true;
+ }
+
+ private class DisallowIntercept implements Runnable {
+ @Override
+ public void run() {
+ final ViewParent parent = mSrc.getParent();
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ private class TriggerLongPress implements Runnable {
+ @Override
+ public void run() {
+ onLongPress();
+ }
+ }
+ }
+
+ /**
+ *
Wrapper class for a ListView. This wrapper can hijack the focus to
+ * make sure the list uses the appropriate drawables and states when
+ * displayed on screen within a drop down. The focus is never actually
+ * passed to the drop down in this mode; the list only looks focused.
+ */
+ private static class DropDownListView extends ListView {
+
+ /*
+ * WARNING: This is a workaround for a touch mode issue.
+ *
+ * Touch mode is propagated lazily to windows. This causes problems in
+ * the following scenario:
+ * - Type something in the AutoCompleteTextView and get some results
+ * - Move down with the d-pad to select an item in the list
+ * - Move up with the d-pad until the selection disappears
+ * - Type more text in the AutoCompleteTextView *using the soft keyboard*
+ * and get new results; you are now in touch mode
+ * - The selection comes back on the first item in the list, even though
+ * the list is supposed to be in touch mode
+ *
+ * Using the soft keyboard triggers the touch mode change but that change
+ * is propagated to our window only after the first list layout, therefore
+ * after the list attempts to resurrect the selection.
+ *
+ * The trick to work around this issue is to pretend the list is in touch
+ * mode when we know that the selection should not appear, that is when
+ * we know the user moved the selection away from the list.
+ *
+ * This boolean is set to true whenever we explicitly hide the list's
+ * selection and reset to false whenever we know the user moved the
+ * selection back to the list.
+ *
+ * When this boolean is true, isInTouchMode() returns true, otherwise it
+ * returns super.isInTouchMode().
+ */
+ private boolean mListSelectionHidden;
+
+ /**
+ * True if this wrapper should fake focus.
+ */
+ private boolean mHijackFocus;
+
+ /** Whether to force drawing of the pressed state selector. */
+ private boolean mDrawsInPressedState;
+
+ /** Current drag-to-open click animation, if any. */
+ private ViewPropertyAnimatorCompat mClickAnimation;
+
+ /** Helper for drag-to-open auto scrolling. */
+ private ListViewAutoScrollHelper mScrollHelper;
+
+ /**
+ * Creates a new list view wrapper.
+ *
+ * @param context this view's context
+ */
+ public DropDownListView(Context context, boolean hijackFocus) {
+ super(context, null, R.attr.dropDownListViewStyle);
+ mHijackFocus = hijackFocus;
+ setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+ }
+
+ /**
+ * Handles forwarded events.
+ *
+ * @param activePointerId id of the pointer that activated forwarding
+ * @return whether the event was handled
+ */
+ public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
+ boolean handledEvent = true;
+ boolean clearPressedItem = false;
+
+ final int actionMasked = MotionEventCompat.getActionMasked(event);
+ switch (actionMasked) {
+ case MotionEvent.ACTION_CANCEL:
+ handledEvent = false;
+ break;
+ case MotionEvent.ACTION_UP:
+ handledEvent = false;
+ // $FALL-THROUGH$
+ case MotionEvent.ACTION_MOVE:
+ final int activeIndex = event.findPointerIndex(activePointerId);
+ if (activeIndex < 0) {
+ handledEvent = false;
+ break;
+ }
+
+ final int x = (int) event.getX(activeIndex);
+ final int y = (int) event.getY(activeIndex);
+ final int position = pointToPosition(x, y);
+ if (position == INVALID_POSITION) {
+ clearPressedItem = true;
+ break;
+ }
+
+ final View child = getChildAt(position - getFirstVisiblePosition());
+ setPressedItem(child, position, x, y);
+ handledEvent = true;
+
+ if (actionMasked == MotionEvent.ACTION_UP) {
+ clickPressedItem(child, position);
+ }
+ break;
+ }
+
+ // Failure to handle the event cancels forwarding.
+ if (!handledEvent || clearPressedItem) {
+ clearPressedItem();
+ }
+
+ // Manage automatic scrolling.
+ if (handledEvent) {
+ if (mScrollHelper == null) {
+ mScrollHelper = new ListViewAutoScrollHelper(this);
+ }
+ mScrollHelper.setEnabled(true);
+ mScrollHelper.onTouch(this, event);
+ } else if (mScrollHelper != null) {
+ mScrollHelper.setEnabled(false);
+ }
+
+ return handledEvent;
+ }
+
+ /**
+ * Starts an alpha animation on the selector. When the animation ends,
+ * the list performs a click on the item.
+ */
+ private void clickPressedItem(final View child, final int position) {
+ final long id = getItemIdAtPosition(position);
+ performItemClick(child, position, id);
+ }
+
+ private void clearPressedItem() {
+ mDrawsInPressedState = false;
+ setPressed(false);
+ // This will call through to updateSelectorState()
+ drawableStateChanged();
+
+ if (mClickAnimation != null) {
+ mClickAnimation.cancel();
+ mClickAnimation = null;
+ }
+ }
+
+ private void setPressedItem(View child, int position, float x, float y) {
+ mDrawsInPressedState = true;
+
+ // Ordering is essential. First update the pressed state and layout
+ // the children. This will ensure the selector actually gets drawn.
+ setPressed(true);
+ layoutChildren();
+
+ // Ensure that keyboard focus starts from the last touched position.
+ setSelection(position);
+ positionSelectorLikeTouchCompat(position, child, x, y);
+
+ // This needs some explanation. We need to disable the selector for this next call
+ // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat
+ // will draw the selector and bad things happen.
+ setSelectorEnabled(false);
+
+ // Refresh the drawable state to reflect the new pressed state,
+ // which will also update the selector state.
+ refreshDrawableState();
+ }
+
+ @Override
+ protected boolean touchModeDrawsInPressedStateCompat() {
+ return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat();
+ }
+
+ @Override
+ public boolean isInTouchMode() {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
+ }
+
+ /**
+ * Returns the focus state in the drop down.
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasWindowFocus() {
+ return mHijackFocus || super.hasWindowFocus();
+ }
+
+ /**
+ * Returns the focus state in the drop down.
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean isFocused() {
+ return mHijackFocus || super.isFocused();
+ }
+
+ /**
+ * Returns the focus state in the drop down.
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return mHijackFocus || super.hasFocus();
+ }
+
+ }
+
+ private class PopupDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ if (isShowing()) {
+ // Resize the popup to fit new content
+ show();
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ dismiss();
+ }
+ }
+
+ private class ListSelectorHider implements Runnable {
+ public void run() {
+ clearListSelection();
+ }
+ }
+
+ private class ResizePopupRunnable implements Runnable {
+ public void run() {
+ if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() &&
+ mDropDownList.getChildCount() <= mListItemExpandMaximum) {
+ mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ show();
+ }
+ }
+ }
+
+ private class PopupTouchInterceptor implements OnTouchListener {
+ public boolean onTouch(View v, MotionEvent event) {
+ final int action = event.getAction();
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ if (action == MotionEvent.ACTION_DOWN &&
+ mPopup != null && mPopup.isShowing() &&
+ (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
+ mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
+ } else if (action == MotionEvent.ACTION_UP) {
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ }
+ return false;
+ }
+ }
+
+ private class PopupScrollListener implements ListView.OnScrollListener {
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
+ !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
+ mHandler.removeCallbacks(mResizePopupRunnable);
+ mResizePopupRunnable.run();
+ }
+ }
+ }
+
+ private static boolean isConfirmKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER;
+ }
+
+ private void setPopupClipToScreenEnabled(boolean clip) {
+ if (sClipToWindowEnabledMethod != null) {
+ try {
+ sClipToWindowEnabledMethod.invoke(mPopup, clip);
+ } catch (Exception e) {
+ Log.i(TAG, "Could not call setClipToScreenEnabled() on PopupWindow. Oh well.");
+ }
+ } else if(clip && Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
+ mPopup.setClippingEnabled(false);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/material/src/main/java/com/rey/material/widget/ListView.java b/material/src/main/java/com/rey/material/widget/ListView.java
new file mode 100644
index 0000000..6d0e244
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/ListView.java
@@ -0,0 +1,99 @@
+package com.rey.material.widget;
+
+import android.content.Context;
+import android.support.v7.widget.ListViewCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.rey.material.util.ThemeManager;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ViewUtil;
+
+public class ListView extends ListViewCompat implements ThemeManager.OnThemeChangedListener{
+
+ private RecyclerListener mRecyclerListener;
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ public ListView(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public ListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public ListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+
+ super.setRecyclerListener(new RecyclerListener() {
+
+ @Override
+ public void onMovedToScrapHeap(View view) {
+ RippleManager.cancelRipple(view);
+
+ if(mRecyclerListener != null)
+ mRecyclerListener.onMovedToScrapHeap(view);
+ }
+
+ });
+
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void setRecyclerListener(RecyclerListener listener) {
+ mRecyclerListener = listener;
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/MaterialRippleLayout.java b/material/src/main/java/com/rey/material/widget/MaterialRippleLayout.java
new file mode 100644
index 0000000..3efefc0
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/MaterialRippleLayout.java
@@ -0,0 +1,816 @@
+/*
+ * Copyright (C) 2014 Balys Valentukevicius
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rey.material.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Property;
+import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.AdapterView;
+import android.widget.FrameLayout;
+
+import com.rey.material.R;
+
+import static android.view.GestureDetector.SimpleOnGestureListener;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+public class MaterialRippleLayout extends FrameLayout {
+
+ private static final int DEFAULT_DURATION = 350;
+ private static final int DEFAULT_FADE_DURATION = 75;
+ private static final float DEFAULT_DIAMETER_DP = 35;
+ private static final float DEFAULT_ALPHA = 0.2f;
+ private static final int DEFAULT_COLOR = Color.parseColor("#afadad");
+ private static final int DEFAULT_BACKGROUND = Color.TRANSPARENT;
+ private static final boolean DEFAULT_HOVER = true;
+ private static final boolean DEFAULT_DELAY_CLICK = true;
+ private static final boolean DEFAULT_PERSISTENT = false;
+ private static final boolean DEFAULT_SEARCH_ADAPTER = false;
+ private static final boolean DEFAULT_RIPPLE_OVERLAY = false;
+ private static final int DEFAULT_ROUNDED_CORNERS = 0;
+
+ private static final int FADE_EXTRA_DELAY = 50;
+ private static final long HOVER_DURATION = 2500;
+
+ private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Rect bounds = new Rect();
+
+ private int rippleColor;
+ private boolean rippleOverlay;
+ private boolean rippleHover;
+ private int rippleDiameter;
+ private int rippleDuration;
+ private int rippleAlpha;
+ private boolean rippleDelayClick;
+ private int rippleFadeDuration;
+ private boolean ripplePersistent;
+ private Drawable rippleBackground;
+ private boolean rippleInAdapter;
+ private float rippleRoundedCorners;
+
+ private float radius;
+
+ private AdapterView parentAdapter;
+ private View childView;
+
+ private AnimatorSet rippleAnimator;
+ private ObjectAnimator hoverAnimator;
+
+ private Point currentCoords = new Point();
+ private Point previousCoords = new Point();
+
+ private int layerType;
+
+ private boolean eventCancelled;
+ private boolean prepressed;
+ private int positionInAdapter;
+
+ private GestureDetector gestureDetector;
+ private PerformClickEvent pendingClickEvent;
+ private PressedEvent pendingPressEvent;
+
+ public static RippleBuilder on(View view) {
+ return new RippleBuilder(view);
+ }
+
+ public MaterialRippleLayout(Context context) {
+ this(context, null, 0);
+ }
+
+ public MaterialRippleLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MaterialRippleLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setWillNotDraw(false);
+ gestureDetector = new GestureDetector(context, longClickListener);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaterialRippleLayout);
+ rippleColor = a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleColor, DEFAULT_COLOR);
+ rippleDiameter = a.getDimensionPixelSize(
+ R.styleable.MaterialRippleLayout_mrl_rippleDimension,
+ (int) dpToPx(getResources(), DEFAULT_DIAMETER_DP)
+ );
+ rippleOverlay = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleOverlay, DEFAULT_RIPPLE_OVERLAY);
+ rippleHover = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleHover, DEFAULT_HOVER);
+ rippleDuration = a.getInt(R.styleable.MaterialRippleLayout_mrl_rippleDuration, DEFAULT_DURATION);
+ rippleAlpha = (int) (255 * a.getFloat(R.styleable.MaterialRippleLayout_mrl_rippleAlpha, DEFAULT_ALPHA));
+ rippleDelayClick = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleDelayClick, DEFAULT_DELAY_CLICK);
+ rippleFadeDuration = a.getInteger(R.styleable.MaterialRippleLayout_mrl_rippleFadeDuration, DEFAULT_FADE_DURATION);
+ rippleBackground = new ColorDrawable(a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleBackground, DEFAULT_BACKGROUND));
+ ripplePersistent = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_ripplePersistent, DEFAULT_PERSISTENT);
+ rippleInAdapter = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleInAdapter, DEFAULT_SEARCH_ADAPTER);
+ rippleRoundedCorners = a.getDimensionPixelSize(R.styleable.MaterialRippleLayout_mrl_rippleRoundedCorners, DEFAULT_ROUNDED_CORNERS);
+
+ a.recycle();
+
+ paint.setColor(rippleColor);
+ paint.setAlpha(rippleAlpha);
+
+ enableClipPathSupportIfNecessary();
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public T getChildView() {
+ return (T) childView;
+ }
+
+ @Override
+ public final void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("MaterialRippleLayout can host only one child");
+ }
+ //noinspection unchecked
+ childView = child;
+ super.addView(child, index, params);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener onClickListener) {
+ if (childView == null) {
+ throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks");
+ }
+ childView.setOnClickListener(onClickListener);
+ }
+
+ @Override
+ public void setOnLongClickListener(OnLongClickListener onClickListener) {
+ if (childView == null) {
+ throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks");
+ }
+ childView.setOnLongClickListener(onClickListener);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return !findClickableViewInChild(childView, (int) event.getX(), (int) event.getY());
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean superOnTouchEvent = super.onTouchEvent(event);
+
+ if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent;
+
+ boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY());
+
+ if (isEventInBounds) {
+ previousCoords.set(currentCoords.x, currentCoords.y);
+ currentCoords.set((int) event.getX(), (int) event.getY());
+ }
+
+ boolean gestureResult = gestureDetector.onTouchEvent(event);
+ if (gestureResult || hasPerformedLongPress) {
+ return true;
+ } else {
+ int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ pendingClickEvent = new PerformClickEvent();
+
+ if (prepressed) {
+ childView.setPressed(true);
+ postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ childView.setPressed(false);
+ }
+ }, ViewConfiguration.getPressedStateDuration());
+ }
+
+ if (isEventInBounds) {
+ startRipple(pendingClickEvent);
+ } else if (!rippleHover) {
+ setRadius(0);
+ }
+ if (!rippleDelayClick && isEventInBounds) {
+ pendingClickEvent.run();
+ }
+ cancelPressedEvent();
+ break;
+ case MotionEvent.ACTION_DOWN:
+ setPositionInAdapter();
+ eventCancelled = false;
+ pendingPressEvent = new PressedEvent(event);
+ if (isInScrollingContainer()) {
+ cancelPressedEvent();
+ prepressed = true;
+ postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout());
+ } else {
+ pendingPressEvent.run();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (rippleInAdapter) {
+ // dont use current coords in adapter since they tend to jump drastically on scroll
+ currentCoords.set(previousCoords.x, previousCoords.y);
+ previousCoords = new Point();
+ }
+ childView.onTouchEvent(event);
+ if (rippleHover) {
+ if (!prepressed) {
+ startRipple(null);
+ }
+ } else {
+ childView.setPressed(false);
+ }
+ cancelPressedEvent();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (rippleHover) {
+ if (isEventInBounds && !eventCancelled) {
+ invalidate();
+ } else if (!isEventInBounds) {
+ startRipple(null);
+ }
+ }
+
+ if (!isEventInBounds) {
+ cancelPressedEvent();
+ if (hoverAnimator != null) {
+ hoverAnimator.cancel();
+ }
+ childView.onTouchEvent(event);
+ eventCancelled = true;
+ }
+ break;
+ }
+ return true;
+ }
+ }
+
+ private void cancelPressedEvent() {
+ if (pendingPressEvent != null) {
+ removeCallbacks(pendingPressEvent);
+ prepressed = false;
+ }
+ }
+
+ private boolean hasPerformedLongPress;
+ private SimpleOnGestureListener longClickListener = new SimpleOnGestureListener() {
+ public void onLongPress(MotionEvent e) {
+ hasPerformedLongPress = childView.performLongClick();
+ if (hasPerformedLongPress) {
+ if (rippleHover) {
+ startRipple(null);
+ }
+ cancelPressedEvent();
+ }
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ hasPerformedLongPress = false;
+ return super.onDown(e);
+ }
+ };
+
+ private void startHover() {
+ if (eventCancelled) return;
+
+ if (hoverAnimator != null) {
+ hoverAnimator.cancel();
+ }
+ final float radius = (float) (Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2)) * 1.2f);
+ hoverAnimator = ObjectAnimator.ofFloat(this, radiusProperty, rippleDiameter, radius)
+ .setDuration(HOVER_DURATION);
+ hoverAnimator.setInterpolator(new LinearInterpolator());
+ hoverAnimator.start();
+ }
+
+ private void startRipple(final Runnable animationEndRunnable) {
+ if (eventCancelled) return;
+
+ float endRadius = getEndRadius();
+
+ cancelAnimations();
+
+ rippleAnimator = new AnimatorSet();
+ rippleAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!ripplePersistent) {
+ setRadius(0);
+ setRippleAlpha(rippleAlpha);
+ }
+ if (animationEndRunnable != null && rippleDelayClick) {
+ animationEndRunnable.run();
+ }
+ childView.setPressed(false);
+ }
+ });
+
+ ObjectAnimator ripple = ObjectAnimator.ofFloat(this, radiusProperty, radius, endRadius);
+ ripple.setDuration(rippleDuration);
+ ripple.setInterpolator(new DecelerateInterpolator());
+ ObjectAnimator fade = ObjectAnimator.ofInt(this, circleAlphaProperty, rippleAlpha, 0);
+ fade.setDuration(rippleFadeDuration);
+ fade.setInterpolator(new AccelerateInterpolator());
+ fade.setStartDelay(rippleDuration - rippleFadeDuration - FADE_EXTRA_DELAY);
+
+ if (ripplePersistent) {
+ rippleAnimator.play(ripple);
+ } else if (getRadius() > endRadius) {
+ fade.setStartDelay(0);
+ rippleAnimator.play(fade);
+ } else {
+ rippleAnimator.playTogether(ripple, fade);
+ }
+ rippleAnimator.start();
+ }
+
+ private void cancelAnimations() {
+ if (rippleAnimator != null) {
+ rippleAnimator.cancel();
+ rippleAnimator.removeAllListeners();
+ }
+
+ if (hoverAnimator != null) {
+ hoverAnimator.cancel();
+ }
+ }
+
+ private float getEndRadius() {
+ final int width = getWidth();
+ final int height = getHeight();
+
+ final int halfWidth = width / 2;
+ final int halfHeight = height / 2;
+
+ final float radiusX = halfWidth > currentCoords.x ? width - currentCoords.x : currentCoords.x;
+ final float radiusY = halfHeight > currentCoords.y ? height - currentCoords.y : currentCoords.y;
+
+ return (float) Math.sqrt(Math.pow(radiusX, 2) + Math.pow(radiusY, 2)) * 1.2f;
+ }
+
+ private boolean isInScrollingContainer() {
+ ViewParent p = getParent();
+ while (p != null && p instanceof ViewGroup) {
+ if (((ViewGroup) p).shouldDelayChildPressedState()) {
+ return true;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ private AdapterView findParentAdapterView() {
+ if (parentAdapter != null) {
+ return parentAdapter;
+ }
+ ViewParent current = getParent();
+ while (true) {
+ if (current instanceof AdapterView) {
+ parentAdapter = (AdapterView) current;
+ return parentAdapter;
+ } else {
+ try {
+ current = current.getParent();
+ } catch (NullPointerException npe) {
+ throw new RuntimeException("Could not find a parent AdapterView");
+ }
+ }
+ }
+ }
+
+ private void setPositionInAdapter() {
+ if (rippleInAdapter) {
+ positionInAdapter = findParentAdapterView().getPositionForView(MaterialRippleLayout.this);
+ }
+ }
+
+ private boolean adapterPositionChanged() {
+ if (rippleInAdapter) {
+ int newPosition = findParentAdapterView().getPositionForView(MaterialRippleLayout.this);
+ final boolean changed = newPosition != positionInAdapter;
+ positionInAdapter = newPosition;
+ if (changed) {
+ cancelPressedEvent();
+ cancelAnimations();
+ childView.setPressed(false);
+ setRadius(0);
+ }
+ return changed;
+ }
+ return false;
+ }
+
+ private boolean findClickableViewInChild(View view, int x, int y) {
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ final Rect rect = new Rect();
+ child.getHitRect(rect);
+
+ final boolean contains = rect.contains(x, y);
+ if (contains) {
+ return findClickableViewInChild(child, x - rect.left, y - rect.top);
+ }
+ }
+ } else if (view != childView) {
+ return (view.isEnabled() && (view.isClickable() || view.isLongClickable() || view.isFocusableInTouchMode()));
+ }
+
+ return view.isFocusableInTouchMode();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ bounds.set(0, 0, w, h);
+ rippleBackground.setBounds(bounds);
+ }
+
+ @Override
+ public boolean isInEditMode() {
+ return true;
+ }
+
+ /*
+ * Drawing
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ final boolean positionChanged = adapterPositionChanged();
+ if (rippleOverlay) {
+ if (!positionChanged) {
+ rippleBackground.draw(canvas);
+ }
+ super.draw(canvas);
+ if (!positionChanged) {
+ if (rippleRoundedCorners != 0) {
+ Path clipPath = new Path();
+ RectF rect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
+ clipPath.addRoundRect(rect, rippleRoundedCorners, rippleRoundedCorners, Path.Direction.CW);
+ canvas.clipPath(clipPath);
+ }
+ canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint);
+ }
+ } else {
+ if (!positionChanged) {
+ rippleBackground.draw(canvas);
+ canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint);
+ }
+ super.draw(canvas);
+ }
+ }
+
+ /*
+ * Animations
+ */
+ private Property radiusProperty
+ = new Property(Float.class, "radius") {
+ @Override
+ public Float get(MaterialRippleLayout object) {
+ return object.getRadius();
+ }
+
+ @Override
+ public void set(MaterialRippleLayout object, Float value) {
+ object.setRadius(value);
+ }
+ };
+
+ private float getRadius() {
+ return radius;
+ }
+
+
+ public void setRadius(float radius) {
+ this.radius = radius;
+ invalidate();
+ }
+
+ private Property circleAlphaProperty
+ = new Property(Integer.class, "rippleAlpha") {
+ @Override
+ public Integer get(MaterialRippleLayout object) {
+ return object.getRippleAlpha();
+ }
+
+ @Override
+ public void set(MaterialRippleLayout object, Integer value) {
+ object.setRippleAlpha(value);
+ }
+ };
+
+ public int getRippleAlpha() {
+ return paint.getAlpha();
+ }
+
+ public void setRippleAlpha(Integer rippleAlpha) {
+ paint.setAlpha(rippleAlpha);
+ invalidate();
+ }
+
+ /*
+ * Accessor
+ */
+ public void setRippleColor(int rippleColor) {
+ this.rippleColor = rippleColor;
+ paint.setColor(rippleColor);
+ paint.setAlpha(rippleAlpha);
+ invalidate();
+ }
+
+ public void setRippleOverlay(boolean rippleOverlay) {
+ this.rippleOverlay = rippleOverlay;
+ }
+
+ public void setRippleDiameter(int rippleDiameter) {
+ this.rippleDiameter = rippleDiameter;
+ }
+
+ public void setRippleDuration(int rippleDuration) {
+ this.rippleDuration = rippleDuration;
+ }
+
+ public void setRippleBackground(int color) {
+ rippleBackground = new ColorDrawable(color);
+ rippleBackground.setBounds(bounds);
+ invalidate();
+ }
+
+ public void setRippleHover(boolean rippleHover) {
+ this.rippleHover = rippleHover;
+ }
+
+ public void setRippleDelayClick(boolean rippleDelayClick) {
+ this.rippleDelayClick = rippleDelayClick;
+ }
+
+ public void setRippleFadeDuration(int rippleFadeDuration) {
+ this.rippleFadeDuration = rippleFadeDuration;
+ }
+
+ public void setRipplePersistent(boolean ripplePersistent) {
+ this.ripplePersistent = ripplePersistent;
+ }
+
+ public void setRippleInAdapter(boolean rippleInAdapter) {
+ this.rippleInAdapter = rippleInAdapter;
+ }
+
+ public void setRippleRoundedCorners(int rippleRoundedCorner) {
+ this.rippleRoundedCorners = rippleRoundedCorner;
+ enableClipPathSupportIfNecessary();
+ }
+
+ public void setDefaultRippleAlpha(int alpha) {
+ this.rippleAlpha = alpha;
+ paint.setAlpha(alpha);
+ invalidate();
+ }
+
+ public void performRipple() {
+ currentCoords = new Point(getWidth() / 2, getHeight() / 2);
+ startRipple(null);
+ }
+
+ public void performRipple(Point anchor) {
+ currentCoords = new Point(anchor.x, anchor.y);
+ startRipple(null);
+ }
+
+ /**
+ * {@link Canvas#clipPath(Path)} is not supported in hardware accelerated layers
+ * before API 18. Use software layer instead
+ *
+ * https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported
+ */
+ private void enableClipPathSupportIfNecessary() {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ if (rippleRoundedCorners != 0) {
+ layerType = getLayerType();
+ setLayerType(LAYER_TYPE_SOFTWARE, null);
+ } else {
+ setLayerType(layerType, null);
+ }
+ }
+ }
+
+ /*
+ * Helper
+ */
+ private class PerformClickEvent implements Runnable {
+
+ @Override
+ public void run() {
+ if (hasPerformedLongPress) return;
+
+ // if parent is an AdapterView, try to call its ItemClickListener
+ if (getParent() instanceof AdapterView) {
+ clickAdapterView((AdapterView) getParent());
+ } else if (rippleInAdapter) {
+ // find adapter view
+ clickAdapterView(findParentAdapterView());
+ } else {
+ // otherwise, just perform click on child
+ childView.performClick();
+ }
+ }
+
+ private void clickAdapterView(AdapterView parent) {
+ final int position = parent.getPositionForView(MaterialRippleLayout.this);
+ final long itemId = parent.getAdapter() != null
+ ? parent.getAdapter().getItemId(position)
+ : 0;
+ if (position != AdapterView.INVALID_POSITION) {
+ parent.performItemClick(MaterialRippleLayout.this, position, itemId);
+ }
+ }
+ }
+
+ private final class PressedEvent implements Runnable {
+
+ private final MotionEvent event;
+
+ public PressedEvent(MotionEvent event) {
+ this.event = event;
+ }
+
+ @Override
+ public void run() {
+ prepressed = false;
+ childView.setLongClickable(false);//prevent the child's long click,let's the ripple layout call it's performLongClick
+ childView.onTouchEvent(event);
+ childView.setPressed(true);
+ if (rippleHover) {
+ startHover();
+ }
+ }
+ }
+
+ static float dpToPx(Resources resources, float dp) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics());
+ }
+
+ /*
+ * Builder
+ */
+
+ public static class RippleBuilder {
+
+ private final Context context;
+ private final View child;
+
+ private int rippleColor = DEFAULT_COLOR;
+ private boolean rippleOverlay = DEFAULT_RIPPLE_OVERLAY;
+ private boolean rippleHover = DEFAULT_HOVER;
+ private float rippleDiameter = DEFAULT_DIAMETER_DP;
+ private int rippleDuration = DEFAULT_DURATION;
+ private float rippleAlpha = DEFAULT_ALPHA;
+ private boolean rippleDelayClick = DEFAULT_DELAY_CLICK;
+ private int rippleFadeDuration = DEFAULT_FADE_DURATION;
+ private boolean ripplePersistent = DEFAULT_PERSISTENT;
+ private int rippleBackground = DEFAULT_BACKGROUND;
+ private boolean rippleSearchAdapter = DEFAULT_SEARCH_ADAPTER;
+ private float rippleRoundedCorner = DEFAULT_ROUNDED_CORNERS;
+
+ public RippleBuilder(View child) {
+ this.child = child;
+ this.context = child.getContext();
+ }
+
+ public RippleBuilder rippleColor(int color) {
+ this.rippleColor = color;
+ return this;
+ }
+
+ public RippleBuilder rippleOverlay(boolean overlay) {
+ this.rippleOverlay = overlay;
+ return this;
+ }
+
+ public RippleBuilder rippleHover(boolean hover) {
+ this.rippleHover = hover;
+ return this;
+ }
+
+ public RippleBuilder rippleDiameterDp(int diameterDp) {
+ this.rippleDiameter = diameterDp;
+ return this;
+ }
+
+ public RippleBuilder rippleDuration(int duration) {
+ this.rippleDuration = duration;
+ return this;
+ }
+
+ public RippleBuilder rippleAlpha(float alpha) {
+ this.rippleAlpha = 255 * alpha;
+ return this;
+ }
+
+ public RippleBuilder rippleDelayClick(boolean delayClick) {
+ this.rippleDelayClick = delayClick;
+ return this;
+ }
+
+ public RippleBuilder rippleFadeDuration(int fadeDuration) {
+ this.rippleFadeDuration = fadeDuration;
+ return this;
+ }
+
+ public RippleBuilder ripplePersistent(boolean persistent) {
+ this.ripplePersistent = persistent;
+ return this;
+ }
+
+ public RippleBuilder rippleBackground(int color) {
+ this.rippleBackground = color;
+ return this;
+ }
+
+ public RippleBuilder rippleInAdapter(boolean inAdapter) {
+ this.rippleSearchAdapter = inAdapter;
+ return this;
+ }
+
+ public RippleBuilder rippleRoundedCorners(int radiusDp) {
+ this.rippleRoundedCorner = radiusDp;
+ return this;
+ }
+
+ public MaterialRippleLayout create() {
+ MaterialRippleLayout layout = new MaterialRippleLayout(context);
+ layout.setRippleColor(rippleColor);
+ layout.setDefaultRippleAlpha((int) rippleAlpha);
+ layout.setRippleDelayClick(rippleDelayClick);
+ layout.setRippleDiameter((int) dpToPx(context.getResources(), rippleDiameter));
+ layout.setRippleDuration(rippleDuration);
+ layout.setRippleFadeDuration(rippleFadeDuration);
+ layout.setRippleHover(rippleHover);
+ layout.setRipplePersistent(ripplePersistent);
+ layout.setRippleOverlay(rippleOverlay);
+ layout.setRippleBackground(rippleBackground);
+ layout.setRippleInAdapter(rippleSearchAdapter);
+ layout.setRippleRoundedCorners((int) dpToPx(context.getResources(), rippleRoundedCorner));
+
+ ViewGroup.LayoutParams params = child.getLayoutParams();
+ ViewGroup parent = (ViewGroup) child.getParent();
+ int index = 0;
+
+ if (parent != null && parent instanceof MaterialRippleLayout) {
+ throw new IllegalStateException("MaterialRippleLayout could not be created: parent of the view already is a MaterialRippleLayout");
+ }
+
+ if (parent != null) {
+ index = parent.indexOfChild(child);
+ parent.removeView(child);
+ }
+
+ layout.addView(child, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+ if (parent != null) {
+ parent.addView(layout, index, params);
+ }
+
+ return layout;
+ }
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/PopupWindow.java b/material/src/main/java/com/rey/material/widget/PopupWindow.java
new file mode 100644
index 0000000..17bbc24
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/PopupWindow.java
@@ -0,0 +1,52 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.rey.material.R;
+
+public class PopupWindow extends android.widget.PopupWindow {
+
+ private final boolean mOverlapAnchor;
+
+ public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PopupWindow, defStyleAttr, 0);
+ mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);
+ a.recycle();
+
+ }
+
+ @Override
+ public void showAsDropDown(View anchor, int xoff, int yoff) {
+ if (Build.VERSION.SDK_INT < 21 && mOverlapAnchor) {
+ // If we're pre-L, emulate overlapAnchor by modifying the yOff
+ yoff -= anchor.getHeight();
+ }
+ super.showAsDropDown(anchor, xoff, yoff);
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ @Override
+ public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
+ if (Build.VERSION.SDK_INT < 21 && mOverlapAnchor) {
+ // If we're pre-L, emulate overlapAnchor by modifying the yOff
+ yoff -= anchor.getHeight();
+ }
+ super.showAsDropDown(anchor, xoff, yoff, gravity);
+ }
+
+ @Override
+ public void update(View anchor, int xoff, int yoff, int width, int height) {
+ if (Build.VERSION.SDK_INT < 21 && mOverlapAnchor) {
+ // If we're pre-L, emulate overlapAnchor by modifying the yOff
+ yoff -= anchor.getHeight();
+ }
+ super.update(anchor, xoff, yoff, width, height);
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/ProgressView.java b/material/src/main/java/com/rey/material/widget/ProgressView.java
new file mode 100644
index 0000000..919db98
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/ProgressView.java
@@ -0,0 +1,255 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.CircularProgressDrawable;
+import com.rey.material.drawable.LinearProgressDrawable;
+import com.rey.material.util.ViewUtil;
+
+public class ProgressView extends View implements ThemeManager.OnThemeChangedListener{
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ private boolean mAutostart = false;
+ private boolean mCircular = true;
+ private int mProgressId;
+
+ public static final int MODE_DETERMINATE = 0;
+ public static final int MODE_INDETERMINATE = 1;
+ public static final int MODE_BUFFER = 2;
+ public static final int MODE_QUERY = 3;
+
+
+ private Drawable mProgressDrawable;
+
+ public ProgressView(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public ProgressView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public ProgressView(Context context, AttributeSet attrs, int defStyleAttr){
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ProgressView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ private boolean needCreateProgress(boolean circular){
+ if(mProgressDrawable == null)
+ return true;
+
+ if(circular)
+ return !(mProgressDrawable instanceof CircularProgressDrawable);
+ else
+ return !(mProgressDrawable instanceof LinearProgressDrawable);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressView, defStyleAttr, defStyleRes);
+
+ int progressId = 0;
+ int progressMode = -1;
+ float progress = -1;
+ float secondaryProgress = -1;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+
+ if(attr == R.styleable.ProgressView_pv_autostart)
+ mAutostart = a.getBoolean(attr, false);
+ else if(attr == R.styleable.ProgressView_pv_circular)
+ mCircular = a.getBoolean(attr, true);
+ else if(attr == R.styleable.ProgressView_pv_progressStyle)
+ progressId = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.ProgressView_pv_progressMode)
+ progressMode = a.getInteger(attr, 0);
+ else if(attr == R.styleable.ProgressView_pv_progress)
+ progress = a.getFloat(attr, 0);
+ else if(attr == R.styleable.ProgressView_pv_secondaryProgress)
+ secondaryProgress = a.getFloat(attr, 0);
+ }
+
+ a.recycle();
+
+ boolean needStart = false;
+
+ if(needCreateProgress(mCircular)){
+ mProgressId = progressId;
+ if(mProgressId == 0)
+ mProgressId = mCircular ? R.style.Material_Drawable_CircularProgress : R.style.Material_Drawable_LinearProgress;
+
+ needStart = mProgressDrawable != null && ((Animatable)mProgressDrawable).isRunning();
+ mProgressDrawable = mCircular ? new CircularProgressDrawable.Builder(context, mProgressId).build() : new LinearProgressDrawable.Builder(context, mProgressId).build();
+ ViewUtil.setBackground(this, mProgressDrawable);
+ }
+ else if(mProgressId != progressId){
+ mProgressId = progressId;
+ if(mProgressDrawable instanceof CircularProgressDrawable)
+ ((CircularProgressDrawable) mProgressDrawable).applyStyle(context, mProgressId);
+ else
+ ((LinearProgressDrawable)mProgressDrawable).applyStyle(context, mProgressId);
+ }
+
+ if(progressMode >= 0) {
+ if(mProgressDrawable instanceof CircularProgressDrawable)
+ ((CircularProgressDrawable) mProgressDrawable).setProgressMode(progressMode);
+ else
+ ((LinearProgressDrawable)mProgressDrawable).setProgressMode(progressMode);
+ }
+
+ if(progress >= 0)
+ setProgress(progress);
+
+ if(secondaryProgress >= 0)
+ setSecondaryProgress(secondaryProgress);
+
+ if(needStart)
+ start();
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ if(changedView != this)
+ return;
+
+ if (mAutostart) {
+ if (visibility == GONE || visibility == INVISIBLE)
+ stop();
+ else
+ start();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(getVisibility() == View.VISIBLE && mAutostart)
+ start();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (mAutostart)
+ stop();
+
+ super.onDetachedFromWindow();
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ public int getProgressMode(){
+ if(mCircular)
+ return ((CircularProgressDrawable)mProgressDrawable).getProgressMode();
+ else
+ return ((LinearProgressDrawable)mProgressDrawable).getProgressMode();
+ }
+
+ /**
+ * @return The current progress of this view in [0..1] range.
+ */
+ public float getProgress(){
+ if(mCircular)
+ return ((CircularProgressDrawable)mProgressDrawable).getProgress();
+ else
+ return ((LinearProgressDrawable)mProgressDrawable).getProgress();
+ }
+
+ /**
+ * @return The current secondary progress of this view in [0..1] range.
+ */
+ public float getSecondaryProgress(){
+ if(mCircular)
+ return ((CircularProgressDrawable)mProgressDrawable).getSecondaryProgress();
+ else
+ return ((LinearProgressDrawable)mProgressDrawable).getSecondaryProgress();
+ }
+
+ /**
+ * Set the current progress of this view.
+ * @param percent The progress value in [0..1] range.
+ */
+ public void setProgress(float percent){
+ if(mCircular)
+ ((CircularProgressDrawable)mProgressDrawable).setProgress(percent);
+ else
+ ((LinearProgressDrawable)mProgressDrawable).setProgress(percent);
+ }
+
+ /**
+ * Set the current secondary progress of this view.
+ * @param percent The progress value in [0..1] range.
+ */
+ public void setSecondaryProgress(float percent){
+ if(mCircular)
+ ((CircularProgressDrawable)mProgressDrawable).setSecondaryProgress(percent);
+ else
+ ((LinearProgressDrawable)mProgressDrawable).setSecondaryProgress(percent);
+ }
+
+ /**
+ * Start showing progress.
+ */
+ public void start(){
+ if(mProgressDrawable != null)
+ ((Animatable)mProgressDrawable).start();
+ }
+
+ /**
+ * Stop showing progress.
+ */
+ public void stop(){
+ if(mProgressDrawable != null)
+ ((Animatable)mProgressDrawable).stop();
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/RadioButton.java b/material/src/main/java/com/rey/material/widget/RadioButton.java
new file mode 100644
index 0000000..d40c88e
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/RadioButton.java
@@ -0,0 +1,61 @@
+package com.rey.material.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.rey.material.drawable.RadioButtonDrawable;
+
+public class RadioButton extends CompoundButton {
+
+ public RadioButton(Context context) {
+ super(context);
+ }
+
+ public RadioButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public RadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public RadioButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ super.applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ RadioButtonDrawable drawable = new RadioButtonDrawable.Builder(context, attrs, defStyleAttr, defStyleRes).build();
+ drawable.setInEditMode(isInEditMode());
+ drawable.setAnimEnable(false);
+ setButtonDrawable(drawable);
+ drawable.setAnimEnable(true);
+ }
+
+ @Override
+ public void toggle() {
+ // we override to prevent toggle when the radio is already
+ // checked (as opposed to check boxes widgets)
+ if (!isChecked()) {
+ super.toggle();
+ }
+ }
+
+ /**
+ * Change the checked state of this button immediately without showing animation.
+ * @param checked The checked state.
+ */
+ public void setCheckedImmediately(boolean checked){
+ if(getButtonDrawable() instanceof RadioButtonDrawable){
+ RadioButtonDrawable drawable = (RadioButtonDrawable)getButtonDrawable();
+ drawable.setAnimEnable(false);
+ setChecked(checked);
+ drawable.setAnimEnable(true);
+ }
+ else
+ setChecked(checked);
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/Slider.java b/material/src/main/java/com/rey/material/widget/Slider.java
new file mode 100644
index 0000000..65739e4
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/Slider.java
@@ -0,0 +1,1251 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.ColorUtil;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.TypefaceUtil;
+import com.rey.material.util.ViewUtil;
+
+/**
+ * Created by Ret on 3/18/2015.
+ */
+public class Slider extends View implements ThemeManager.OnThemeChangedListener{
+
+ private RippleManager mRippleManager;
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ private Paint mPaint;
+ private RectF mDrawRect;
+ private RectF mTempRect;
+ private Path mLeftTrackPath;
+ private Path mRightTrackPath;
+ private Path mMarkPath;
+
+ private int mMinValue = 0;
+ private int mMaxValue = 100;
+ private int mStepValue = 1;
+
+ private boolean mDiscreteMode = false;
+
+ private int mPrimaryColor;
+ private int mSecondaryColor;
+ private int mTrackSize = -1;
+ private Paint.Cap mTrackCap = Paint.Cap.BUTT;
+ private int mThumbBorderSize = -1;
+ private int mThumbRadius = -1;
+ private int mThumbFocusRadius = -1;
+ private float mThumbPosition = -1;
+ private Typeface mTypeface = Typeface.DEFAULT;
+ private int mTextSize = -1;
+ private int mTextColor = 0xFFFFFFFF;
+ private int mGravity = Gravity.CENTER;
+ private int mTravelAnimationDuration = -1;
+ private int mTransformAnimationDuration = -1;
+ private Interpolator mInterpolator;
+ private int mBaselineOffset;
+
+ private int mTouchSlop;
+ private PointF mMemoPoint;
+ private boolean mIsDragging;
+ private float mThumbCurrentRadius;
+ private float mThumbFillPercent;
+ private boolean mAlwaysFillThumb = false;
+ private int mTextHeight;
+ private int mMemoValue;
+ private String mValueText;
+
+ private ThumbRadiusAnimator mThumbRadiusAnimator;
+ private ThumbStrokeAnimator mThumbStrokeAnimator;
+ private ThumbMoveAnimator mThumbMoveAnimator;
+
+ private boolean mIsRtl = false;
+
+ /**
+ * Interface definition for a callback to be invoked when thumb's position changed.
+ */
+ public interface OnPositionChangeListener{
+ /**
+ * Called when thumb's position changed.
+ *
+ * @param view The view fire this event.
+ * @param fromUser Indicate the change is from user touch event or not.
+ * @param oldPos The old position of thumb.
+ * @param newPos The new position of thumb.
+ * @param oldValue The old value.
+ * @param newValue The new value.
+ */
+ public void onPositionChanged(Slider view, boolean fromUser, float oldPos, float newPos, int oldValue, int newValue);
+ }
+
+ private OnPositionChangeListener mOnPositionChangeListener;
+
+ public interface ValueDescriptionProvider{
+
+ public String getDescription(int value);
+
+ }
+
+ private ValueDescriptionProvider mValueDescriptionProvider;
+
+ public Slider(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public Slider(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public Slider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public Slider(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ //default color
+ mPrimaryColor = ThemeUtil.colorControlActivated(context, 0xFF000000);
+ mSecondaryColor = ThemeUtil.colorControlNormal(context, 0xFF000000);
+
+ mDrawRect = new RectF();
+ mTempRect = new RectF();
+ mLeftTrackPath = new Path();
+ mRightTrackPath = new Path();
+
+ mThumbRadiusAnimator = new ThumbRadiusAnimator();
+ mThumbStrokeAnimator = new ThumbStrokeAnimator();
+ mThumbMoveAnimator = new ThumbMoveAnimator();
+
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mMemoPoint = new PointF();
+
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider, defStyleAttr, defStyleRes);
+ int minValue = getMinValue();
+ int maxValue = getMaxValue();
+ boolean valueRangeDefined = false;
+ int value = -1;
+ boolean valueDefined = false;
+ String familyName = null;
+ int style = Typeface.NORMAL;
+ boolean textStyleDefined = false;
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+ if(attr == R.styleable.Slider_sl_discreteMode)
+ mDiscreteMode = a.getBoolean(attr, false);
+ else if(attr == R.styleable.Slider_sl_primaryColor)
+ mPrimaryColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.Slider_sl_secondaryColor)
+ mSecondaryColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.Slider_sl_trackSize)
+ mTrackSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Slider_sl_trackCap) {
+ int cap = a.getInteger(attr, 0);
+ if(cap == 0)
+ mTrackCap = Paint.Cap.BUTT;
+ else if(cap == 1)
+ mTrackCap = Paint.Cap.ROUND;
+ else
+ mTrackCap = Paint.Cap.SQUARE;
+ }
+ else if(attr == R.styleable.Slider_sl_thumbBorderSize)
+ mThumbBorderSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Slider_sl_thumbRadius)
+ mThumbRadius = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Slider_sl_thumbFocusRadius)
+ mThumbFocusRadius = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Slider_sl_travelAnimDuration) {
+ mTravelAnimationDuration = a.getInteger(attr, 0);
+ mTransformAnimationDuration = mTravelAnimationDuration;
+ }
+ else if(attr == R.styleable.Slider_sl_alwaysFillThumb) {
+ mAlwaysFillThumb = a.getBoolean(R.styleable.Slider_sl_alwaysFillThumb, false);
+ }
+ else if(attr == R.styleable.Slider_sl_interpolator){
+ int resId = a.getResourceId(R.styleable.Slider_sl_interpolator, 0);
+ mInterpolator = AnimationUtils.loadInterpolator(context, resId);
+ }
+ else if(attr == R.styleable.Slider_android_gravity)
+ mGravity = a.getInteger(attr, 0);
+ else if(attr == R.styleable.Slider_sl_minValue) {
+ minValue = a.getInteger(attr, 0);
+ valueRangeDefined = true;
+ }
+ else if(attr == R.styleable.Slider_sl_maxValue) {
+ maxValue = a.getInteger(attr, 0);
+ valueRangeDefined = true;
+ }
+ else if(attr == R.styleable.Slider_sl_stepValue)
+ mStepValue = a.getInteger(attr, 0);
+ else if(attr == R.styleable.Slider_sl_value) {
+ value = a.getInteger(attr, 0);
+ valueDefined = true;
+ }
+ else if(attr == R.styleable.Slider_sl_fontFamily) {
+ familyName = a.getString(attr);
+ textStyleDefined = true;
+ }
+ else if(attr == R.styleable.Slider_sl_textStyle) {
+ style = a.getInteger(attr, 0);
+ textStyleDefined = true;
+ }
+ else if(attr == R.styleable.Slider_sl_textColor)
+ mTextColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.Slider_sl_textSize)
+ mTextSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Slider_android_enabled)
+ setEnabled(a.getBoolean(attr, true));
+ else if(attr == R.styleable.Slider_sl_baselineOffset)
+ mBaselineOffset = a.getDimensionPixelOffset(attr, 0);
+ }
+
+ a.recycle();
+
+ if(mTrackSize < 0)
+ mTrackSize = ThemeUtil.dpToPx(context, 2);
+
+ if(mThumbBorderSize < 0)
+ mThumbBorderSize = ThemeUtil.dpToPx(context, 2);
+
+ if(mThumbRadius < 0)
+ mThumbRadius = ThemeUtil.dpToPx(context, 10);
+
+ if(mThumbFocusRadius < 0)
+ mThumbFocusRadius = ThemeUtil.dpToPx(context, 14);
+
+ if(mTravelAnimationDuration < 0){
+ mTravelAnimationDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+ mTransformAnimationDuration = mTravelAnimationDuration;
+ }
+
+ if(mInterpolator == null)
+ mInterpolator = new DecelerateInterpolator();
+
+ if(valueRangeDefined)
+ setValueRange(minValue, maxValue, false);
+
+ if(valueDefined)
+ setValue(value, false);
+ else if(mThumbPosition < 0)
+ setValue(mMinValue, false);
+
+ if(textStyleDefined)
+ mTypeface = TypefaceUtil.load(context, familyName, style);
+
+ if(mTextSize < 0)
+ mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_small_material);
+
+ mPaint.setTextSize(mTextSize);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+ mPaint.setTypeface(mTypeface);
+
+ measureText();
+ invalidate();
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ private void measureText(){
+ if(mValueText == null)
+ return;
+
+ Rect temp = new Rect();
+ mPaint.setTextSize(mTextSize);
+ float width = mPaint.measureText(mValueText);
+ float maxWidth = (float)(mThumbRadius * Math.sqrt(2) * 2 - ThemeUtil.dpToPx(getContext(), 8));
+ if(width > maxWidth){
+ float textSize = mTextSize * maxWidth / width;
+ mPaint.setTextSize(textSize);
+ }
+
+ mPaint.getTextBounds(mValueText, 0, mValueText.length(), temp);
+ mTextHeight = temp.height();
+ }
+
+ private String getValueText(){
+ int value = getValue();
+ if(mValueText == null || mMemoValue != value){
+ mMemoValue = value;
+ mValueText = mValueDescriptionProvider == null ? String.valueOf(mMemoValue) : mValueDescriptionProvider.getDescription(mMemoValue);
+ measureText();
+ }
+
+ return mValueText;
+ }
+
+ /**
+ * @return The minimum selectable value.
+ */
+ public int getMinValue(){
+ return mMinValue;
+ }
+
+ /**
+ * @return The maximum selectable value.
+ */
+ public int getMaxValue(){
+ return mMaxValue;
+ }
+
+ /**
+ * @return The step value.
+ */
+ public int getStepValue(){
+ return mStepValue;
+ }
+
+ /**
+ * Set the randge of selectable value.
+ * @param min The minimum selectable value.
+ * @param max The maximum selectable value.
+ * @param animation Indicate that should show animation when thumb's current position changed.
+ */
+ public void setValueRange(int min, int max, boolean animation){
+ if(max < min || (min == mMinValue && max == mMaxValue))
+ return;
+
+ float oldValue = getExactValue();
+ float oldPosition = getPosition();
+ mMinValue = min;
+ mMaxValue = max;
+
+ setValue(oldValue, animation);
+ if(mOnPositionChangeListener != null && oldPosition == getPosition() && oldValue != getExactValue())
+ mOnPositionChangeListener.onPositionChanged(this, false, oldPosition, oldPosition, Math.round(oldValue), getValue());
+ }
+
+ /**
+ * @return The selected value.
+ */
+ public int getValue(){
+ return Math.round(getExactValue());
+ }
+
+ /**
+ * @return The exact selected value.
+ */
+ public float getExactValue(){
+ return (mMaxValue - mMinValue) * getPosition() + mMinValue;
+ }
+
+ /**
+ * @return The current position of thumb in [0..1] range.
+ */
+ public float getPosition(){
+ return mThumbMoveAnimator.isRunning() ? mThumbMoveAnimator.getPosition() : mThumbPosition;
+ }
+
+ /**
+ * Set current position of thumb.
+ * @param pos The position in [0..1] range.
+ * @param animation Indicate that should show animation when change thumb's position.
+ */
+ public void setPosition(float pos, boolean animation){
+ setPosition(pos, animation, animation, false);
+ }
+
+ private void setPosition(float pos, boolean moveAnimation, boolean transformAnimation, boolean fromUser){
+ boolean change = getPosition() != pos;
+ int oldValue = getValue();
+ float oldPos = getPosition();
+
+ if(!moveAnimation || !mThumbMoveAnimator.startAnimation(pos)){
+ mThumbPosition = pos;
+
+ if(transformAnimation) {
+ if(!mIsDragging)
+ mThumbRadiusAnimator.startAnimation(mThumbRadius);
+ mThumbStrokeAnimator.startAnimation(pos == 0 ? 0 : 1);
+ }
+ else{
+ mThumbCurrentRadius = mThumbRadius;
+ mThumbFillPercent = (mAlwaysFillThumb || mThumbPosition != 0) ? 1 : 0;
+ invalidate();
+ }
+ }
+
+ int newValue = getValue();
+ float newPos = getPosition();
+
+ if(change && mOnPositionChangeListener != null)
+ mOnPositionChangeListener.onPositionChanged(this, fromUser, oldPos, newPos, oldValue, newValue);
+ }
+
+ /**
+ * Changes the primary color and invalidates the view to force a redraw.
+ * @param color New color to assign to mPrimaryColor.
+ */
+ public void setPrimaryColor(int color) {
+ mPrimaryColor = color;
+ invalidate();
+ }
+
+ /**
+ * Changes the secondary color and invalidates the view to force a redraw.
+ * @param color New color to assign to mSecondaryColor.
+ */
+ public void setSecondaryColor(int color) {
+ mSecondaryColor = color;
+ invalidate();
+ }
+
+ /**
+ * Set if we want the thumb to always be filled.
+ * @param alwaysFillThumb Do we want it to always be filled.
+ */
+ public void setAlwaysFillThumb(boolean alwaysFillThumb) {
+ mAlwaysFillThumb = alwaysFillThumb;
+ }
+
+ /**
+ * Set the selected value of this Slider.
+ * @param value The selected value.
+ * @param animation Indicate that should show animation when change thumb's position.
+ */
+ public void setValue(float value, boolean animation){
+ value = Math.min(mMaxValue, Math.max(value, mMinValue));
+ setPosition((value - mMinValue) / (mMaxValue - mMinValue), animation);
+ }
+
+ /**
+ * Set a listener will be called when thumb's position changed.
+ * @param listener The {@link OnPositionChangeListener} will be called.
+ */
+ public void setOnPositionChangeListener(OnPositionChangeListener listener){
+ mOnPositionChangeListener = listener;
+ }
+
+ public void setValueDescriptionProvider(ValueDescriptionProvider provider){
+ mValueDescriptionProvider = provider;
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable drawable) {
+ Drawable background = getBackground();
+ if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
+ ((RippleDrawable) background).setBackgroundDrawable(drawable);
+ else
+ super.setBackgroundDrawable(drawable);
+ }
+
+ protected RippleManager getRippleManager(){
+ if(mRippleManager == null){
+ synchronized (RippleManager.class){
+ if(mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ switch (widthMode) {
+ case MeasureSpec.UNSPECIFIED:
+ widthSize = getSuggestedMinimumWidth();
+ break;
+ case MeasureSpec.AT_MOST:
+ widthSize = Math.min(widthSize, getSuggestedMinimumWidth());
+ break;
+ }
+
+ switch (heightMode) {
+ case MeasureSpec.UNSPECIFIED:
+ heightSize = getSuggestedMinimumHeight();
+ break;
+ case MeasureSpec.AT_MOST:
+ heightSize = Math.min(heightSize, getSuggestedMinimumHeight());
+ break;
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ @Override
+ public int getSuggestedMinimumWidth() {
+ return (mDiscreteMode ? (int)(mThumbRadius * Math.sqrt(2)) : mThumbFocusRadius) * 4 + getPaddingLeft() + getPaddingRight();
+ }
+
+ @Override
+ public int getSuggestedMinimumHeight() {
+ return (mDiscreteMode ? (int)(mThumbRadius * (4 + Math.sqrt(2))) : mThumbFocusRadius * 2) + getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
+ if(mIsRtl != rtl) {
+ mIsRtl = rtl;
+ invalidate();
+ }
+ }
+
+ @Override
+ public int getBaseline() {
+ int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ int baseline;
+
+ if(mDiscreteMode){
+ int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2)));
+ int height = mThumbRadius * 2;
+ switch (align) {
+ case Gravity.TOP:
+ baseline = Math.max(getPaddingTop(), fullHeight - height) + mThumbRadius;
+ break;
+ case Gravity.BOTTOM:
+ baseline = getMeasuredHeight() - getPaddingBottom();
+ break;
+ default:
+ baseline = Math.round(Math.max((getMeasuredHeight() - height) / 2f, fullHeight - height) + mThumbRadius);
+ break;
+ }
+ }
+ else{
+ int height = mThumbFocusRadius * 2;
+ switch (align) {
+ case Gravity.TOP:
+ baseline = getPaddingTop() + mThumbFocusRadius;
+ break;
+ case Gravity.BOTTOM:
+ baseline = getMeasuredHeight() - getPaddingBottom();
+ break;
+ default:
+ baseline = Math.round((getMeasuredHeight() - height) / 2f + mThumbFocusRadius);
+ break;
+ }
+ }
+
+ return baseline + mBaselineOffset;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mDrawRect.left = getPaddingLeft() + mThumbRadius;
+ mDrawRect.right = w - getPaddingRight() - mThumbRadius;
+
+ int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ if(mDiscreteMode){
+ int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2)));
+ int height = mThumbRadius * 2;
+ switch (align) {
+ case Gravity.TOP:
+ mDrawRect.top = Math.max(getPaddingTop(), fullHeight - height);
+ mDrawRect.bottom = mDrawRect.top + height;
+ break;
+ case Gravity.BOTTOM:
+ mDrawRect.bottom = h - getPaddingBottom();
+ mDrawRect.top = mDrawRect.bottom - height;
+ break;
+ default:
+ mDrawRect.top = Math.max((h - height) / 2f, fullHeight - height);
+ mDrawRect.bottom = mDrawRect.top + height;
+ break;
+ }
+ }
+ else{
+ int height = mThumbFocusRadius * 2;
+ switch (align) {
+ case Gravity.TOP:
+ mDrawRect.top = getPaddingTop();
+ mDrawRect.bottom = mDrawRect.top + height;
+ break;
+ case Gravity.BOTTOM:
+ mDrawRect.bottom = h - getPaddingBottom();
+ mDrawRect.top = mDrawRect.bottom - height;
+ break;
+ default:
+ mDrawRect.top = (h - height) / 2f;
+ mDrawRect.bottom = mDrawRect.top + height;
+ break;
+ }
+ }
+ }
+
+ private boolean isThumbHit(float x, float y, float radius){
+ float cx = mDrawRect.width() * mThumbPosition + mDrawRect.left;
+ float cy = mDrawRect.centerY();
+
+ return x >= cx - radius && x <= cx + radius && y >= cy - radius && y < cy + radius;
+ }
+
+ private double distance(float x1, float y1, float x2, float y2){
+ return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
+ }
+
+ private float correctPosition(float position){
+ if(!mDiscreteMode)
+ return position;
+
+ int totalOffset = mMaxValue - mMinValue;
+ int valueOffset = Math.round(totalOffset * position);
+ int stepOffset = valueOffset / mStepValue;
+ int lowerValue = stepOffset * mStepValue;
+ int higherValue = Math.min(totalOffset, (stepOffset + 1) * mStepValue);
+
+ if(valueOffset - lowerValue < higherValue - valueOffset)
+ position = lowerValue / (float)totalOffset;
+ else
+ position = higherValue / (float)totalOffset;
+
+ return position;
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ super.onTouchEvent(event);
+ getRippleManager().onTouchEvent(this, event);
+
+ if(!isEnabled())
+ return false;
+
+ float x = event.getX();
+ float y = event.getY();
+ if(mIsRtl)
+ x = 2 * mDrawRect.centerX() - x;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mIsDragging = isThumbHit(x, y, mThumbRadius) && !mThumbMoveAnimator.isRunning();
+ mMemoPoint.set(x, y);
+ if(mIsDragging) {
+ mThumbRadiusAnimator.startAnimation(mDiscreteMode ? 0 : mThumbFocusRadius);
+
+ if(getParent() != null)
+ getParent().requestDisallowInterceptTouchEvent(true);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if(mIsDragging) {
+ if(mDiscreteMode) {
+ float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
+ setPosition(position, true, true, true);
+ }
+ else{
+ float offset = (x - mMemoPoint.x) / mDrawRect.width();
+ float position = Math.min(1f, Math.max(0f, mThumbPosition + offset));
+ setPosition(position, false, true, true);
+ mMemoPoint.x = x;
+ invalidate();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if(mIsDragging) {
+ mIsDragging = false;
+ setPosition(getPosition(), true, true, true);
+
+ if(getParent() != null)
+ getParent().requestDisallowInterceptTouchEvent(false);
+ }
+ else if(distance(mMemoPoint.x, mMemoPoint.y, x, y) <= mTouchSlop){
+ float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
+ setPosition(position, true, true, true);
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if(mIsDragging) {
+ mIsDragging = false;
+ setPosition(getPosition(), true, true, true);
+
+ if(getParent() != null)
+ getParent().requestDisallowInterceptTouchEvent(false);
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ private void getTrackPath(float x, float y, float radius){
+ float halfStroke = mTrackSize / 2f;
+
+ mLeftTrackPath.reset();
+ mRightTrackPath.reset();
+
+ if(radius - 1f < halfStroke){
+ if(mTrackCap != Paint.Cap.ROUND){
+ if(x > mDrawRect.left){
+ mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke);
+ mLeftTrackPath.lineTo(x, y - halfStroke);
+ mLeftTrackPath.lineTo(x, y + halfStroke);
+ mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke);
+ mLeftTrackPath.close();
+ }
+
+ if(x < mDrawRect.right){
+ mRightTrackPath.moveTo(mDrawRect.right, y + halfStroke);
+ mRightTrackPath.lineTo(x, y + halfStroke);
+ mRightTrackPath.lineTo(x, y - halfStroke);
+ mRightTrackPath.lineTo(mDrawRect.right, y - halfStroke);
+ mRightTrackPath.close();
+ }
+ }
+ else{
+ if(x > mDrawRect.left){
+ mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
+ mLeftTrackPath.arcTo(mTempRect, 90, 180);
+ mLeftTrackPath.lineTo(x, y - halfStroke);
+ mLeftTrackPath.lineTo(x, y + halfStroke);
+ mLeftTrackPath.close();
+ }
+
+ if(x < mDrawRect.right){
+ mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
+ mRightTrackPath.arcTo(mTempRect, 270, 180);
+ mRightTrackPath.lineTo(x, y + halfStroke);
+ mRightTrackPath.lineTo(x, y - halfStroke);
+ mRightTrackPath.close();
+ }
+ }
+ }
+ else{
+ if(mTrackCap != Paint.Cap.ROUND){
+ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
+ float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);
+
+ if(x - radius > mDrawRect.left){
+ mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke);
+ mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
+ mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke);
+ mLeftTrackPath.close();
+ }
+
+ if(x + radius < mDrawRect.right){
+ mRightTrackPath.moveTo(mDrawRect.right, y - halfStroke);
+ mRightTrackPath.arcTo(mTempRect, -angle, angle * 2);
+ mRightTrackPath.lineTo(mDrawRect.right, y + halfStroke);
+ mRightTrackPath.close();
+ }
+ }
+ else{
+ float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);
+
+ if(x - radius > mDrawRect.left){
+ float angle2 = (float)(Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180);
+
+ mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
+ mLeftTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2);
+
+ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
+ mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
+ mLeftTrackPath.close();
+ }
+
+ if(x + radius < mDrawRect.right){
+ float angle2 = (float)Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke));
+ mRightTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke));
+
+ angle2 = (float)(angle2 / Math.PI * 180);
+ mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
+ mRightTrackPath.arcTo(mTempRect, angle2, -angle2 * 2);
+
+ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
+ mRightTrackPath.arcTo(mTempRect, -angle, angle * 2);
+ mRightTrackPath.close();
+ }
+ }
+ }
+ }
+
+ private Path getMarkPath(Path path, float cx, float cy, float radius, float factor){
+ if(path == null)
+ path = new Path();
+ else
+ path.reset();
+
+ float x1 = cx - radius;
+ float y1 = cy;
+ float x2 = cx + radius;
+ float y2 = cy;
+ float x3 = cx;
+ float y3 = cy + radius;
+
+ float nCx = cx;
+ float nCy = cy - radius * factor;
+
+ // calculate first arc
+ float angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
+ float nRadius = (float)distance(nCx, nCy, x1, y1);
+ mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
+ path.moveTo(x1, y1);
+ path.arcTo(mTempRect, 180 - angle, 180 + angle * 2);
+
+ if(factor > 0.9f)
+ path.lineTo(x3, y3);
+ else{
+ // find center point for second arc
+ float x4 = (x2 + x3) / 2;
+ float y4 = (y2 + y3) / 2;
+
+ double d1 = distance(x2, y2, x4, y4);
+ double d2 = d1 / Math.tan(Math.PI * (1f - factor) / 4);
+
+ nCx = (float)(x4 - Math.cos(Math.PI / 4) * d2);
+ nCy = (float)(y4 - Math.sin(Math.PI / 4) * d2);
+
+ // calculate second arc
+ angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
+ float angle2 = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
+ nRadius = (float)distance(nCx, nCy, x2, y2);
+ mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
+ path.arcTo(mTempRect, angle, angle2 - angle);
+
+ // calculate third arc
+ nCx = cx * 2 - nCx;
+ angle = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
+ angle2 = (float)(Math.atan2(y1 - nCy, x1 - nCx) * 180 / Math.PI);
+ mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
+ path.arcTo(mTempRect, angle + (float)Math.PI / 4, angle2 - angle);
+ }
+
+ path.close();
+
+ return path;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+
+ float x = mDrawRect.width() * mThumbPosition + mDrawRect.left;
+ if(mIsRtl)
+ x = 2 * mDrawRect.centerX() - x;
+ float y = mDrawRect.centerY();
+ int filledPrimaryColor = ColorUtil.getMiddleColor(mSecondaryColor, isEnabled() ? mPrimaryColor : mSecondaryColor, mThumbFillPercent);
+
+ getTrackPath(x, y, mThumbCurrentRadius);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(mIsRtl ? filledPrimaryColor : mSecondaryColor);
+ canvas.drawPath(mRightTrackPath, mPaint);
+ mPaint.setColor(mIsRtl ? mSecondaryColor : filledPrimaryColor);
+ canvas.drawPath(mLeftTrackPath, mPaint);
+
+ mPaint.setColor(filledPrimaryColor);
+ if(mDiscreteMode){
+ float factor = 1f - mThumbCurrentRadius / mThumbRadius;
+
+ if(factor > 0){
+ mMarkPath = getMarkPath(mMarkPath, x, y, mThumbRadius, factor);
+ mPaint.setStyle(Paint.Style.FILL);
+ int saveCount = canvas.save();
+ canvas.translate(0, -mThumbRadius * 2 * factor);
+ canvas.drawPath(mMarkPath, mPaint);
+ mPaint.setColor(ColorUtil.getColor(mTextColor, factor));
+ canvas.drawText(getValueText(), x, y + mTextHeight / 2f - mThumbRadius * factor, mPaint);
+ canvas.restoreToCount(saveCount);
+ }
+
+ float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
+ if(radius > 0) {
+ mPaint.setColor(filledPrimaryColor);
+ canvas.drawCircle(x, y, radius, mPaint);
+ }
+ }
+ else{
+ float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
+ if(mThumbFillPercent == 1)
+ mPaint.setStyle(Paint.Style.FILL);
+ else{
+ float strokeWidth = (radius - mThumbBorderSize) * mThumbFillPercent + mThumbBorderSize;
+ radius = radius - strokeWidth / 2f;
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(strokeWidth);
+ }
+ canvas.drawCircle(x, y, radius, mPaint);
+ }
+ }
+
+ class ThumbRadiusAnimator implements Runnable{
+
+ boolean mRunning = false;
+ long mStartTime;
+ float mStartRadius;
+ int mRadius;
+
+ public void resetAnimation(){
+ mStartTime = SystemClock.uptimeMillis();
+ mStartRadius = mThumbCurrentRadius;
+ }
+
+ public boolean startAnimation(int radius) {
+ if(mThumbCurrentRadius == radius)
+ return false;
+
+ mRadius = radius;
+
+ if(getHandler() != null){
+ resetAnimation();
+ mRunning = true;
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ invalidate();
+ return true;
+ }
+ else {
+ mThumbCurrentRadius = mRadius;
+ invalidate();
+ return false;
+ }
+ }
+
+ public void stopAnimation() {
+ mRunning = false;
+ mThumbCurrentRadius = mRadius;
+ if(getHandler() != null)
+ getHandler().removeCallbacks(this);
+ invalidate();
+ }
+
+ @Override
+ public void run() {
+ long curTime = SystemClock.uptimeMillis();
+ float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration);
+ float value = mInterpolator.getInterpolation(progress);
+
+ mThumbCurrentRadius = (mRadius - mStartRadius) * value + mStartRadius;
+
+ if(progress == 1f)
+ stopAnimation();
+
+ if(mRunning) {
+ if(getHandler() != null)
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ else
+ stopAnimation();
+ }
+
+ invalidate();
+ }
+
+ }
+
+ class ThumbStrokeAnimator implements Runnable{
+
+ boolean mRunning = false;
+ long mStartTime;
+ float mStartFillPercent;
+ int mFillPercent;
+
+ public void resetAnimation(){
+ mStartTime = SystemClock.uptimeMillis();
+ mStartFillPercent = mThumbFillPercent;
+ }
+
+ public boolean startAnimation(int fillPercent) {
+ if(mThumbFillPercent == fillPercent)
+ return false;
+
+ mFillPercent = fillPercent;
+
+ if(getHandler() != null){
+ resetAnimation();
+ mRunning = true;
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ invalidate();
+ return true;
+ }
+ else {
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
+ invalidate();
+ return false;
+ }
+ }
+
+ public void stopAnimation() {
+ mRunning = false;
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
+ if(getHandler() != null)
+ getHandler().removeCallbacks(this);
+ invalidate();
+ }
+
+ @Override
+ public void run() {
+ long curTime = SystemClock.uptimeMillis();
+ float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration);
+ float value = mInterpolator.getInterpolation(progress);
+
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
+
+ if(progress == 1f)
+ stopAnimation();
+
+ if(mRunning) {
+ if(getHandler() != null)
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ else
+ stopAnimation();
+ }
+
+ invalidate();
+ }
+
+ }
+
+ class ThumbMoveAnimator implements Runnable{
+
+ boolean mRunning = false;
+ long mStartTime;
+ float mStartFillPercent;
+ float mStartRadius;
+ float mStartPosition;
+ float mPosition;
+ float mFillPercent;
+ int mDuration;
+
+ public boolean isRunning(){
+ return mRunning;
+ }
+
+ public float getPosition(){
+ return mPosition;
+ }
+
+ public void resetAnimation(){
+ mStartTime = SystemClock.uptimeMillis();
+ mStartPosition = mThumbPosition;
+ mStartFillPercent = mThumbFillPercent;
+ mStartRadius = mThumbCurrentRadius;
+ mFillPercent = mPosition == 0 ? 0 : 1;
+ mDuration = mDiscreteMode && !mIsDragging ? mTransformAnimationDuration * 2 + mTravelAnimationDuration : mTravelAnimationDuration;
+ }
+
+ public boolean startAnimation(float position) {
+ if(mThumbPosition == position)
+ return false;
+
+ mPosition = position;
+
+ if(getHandler() != null){
+ resetAnimation();
+ mRunning = true;
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ invalidate();
+ return true;
+ }
+ else {
+ mThumbPosition = position;
+ invalidate();
+ return false;
+ }
+ }
+
+ public void stopAnimation() {
+ mRunning = false;
+ mThumbCurrentRadius = mDiscreteMode && mIsDragging ? 0 : mThumbRadius;
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
+ mThumbPosition = mPosition;
+ if(getHandler() != null)
+ getHandler().removeCallbacks(this);
+ invalidate();
+ }
+
+ @Override
+ public void run() {
+ long curTime = SystemClock.uptimeMillis();
+ float progress = Math.min(1f, (float)(curTime - mStartTime) / mDuration);
+ float value = mInterpolator.getInterpolation(progress);
+
+ if(mDiscreteMode){
+ if(mIsDragging) {
+ mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
+ }
+ else{
+ float p1 = (float)mTravelAnimationDuration / mDuration;
+ float p2 = (float)(mTravelAnimationDuration + mTransformAnimationDuration)/ mDuration;
+ if(progress < p1) {
+ value = mInterpolator.getInterpolation(progress / p1);
+ mThumbCurrentRadius = mStartRadius * (1f - value);
+ mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
+ }
+ else if(progress > p2){
+ mThumbCurrentRadius = mThumbRadius * (progress - p2) / (1 - p2);
+ }
+ }
+ }
+ else{
+ mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
+ mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
+
+ if(progress < 0.2)
+ mThumbCurrentRadius = Math.max(mThumbRadius + mThumbBorderSize * progress * 5, mThumbCurrentRadius);
+ else if(progress >= 0.8)
+ mThumbCurrentRadius = mThumbRadius + mThumbBorderSize * (5f - progress * 5);
+ }
+
+
+ if(progress == 1f)
+ stopAnimation();
+
+ if(mRunning) {
+ if(getHandler() != null)
+ getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ else
+ stopAnimation();
+ }
+
+ invalidate();
+ }
+
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ ss.position = getPosition();
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+ setPosition(ss.position, false);
+ requestLayout();
+ }
+
+ static class SavedState extends BaseSavedState {
+ float position;
+
+ /**
+ * Constructor called from {@link Slider#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ position = in.readFloat();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeFloat(position);
+ }
+
+ @Override
+ public String toString() {
+ return "Slider.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " pos=" + position + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/SnackBar.java b/material/src/main/java/com/rey/material/widget/SnackBar.java
new file mode 100644
index 0000000..920fe93
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/SnackBar.java
@@ -0,0 +1,976 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.ViewUtil;
+
+@SuppressWarnings("unused")
+public class SnackBar extends FrameLayout implements ThemeManager.OnThemeChangedListener{
+
+ private TextView mText;
+ private Button mAction;
+
+ public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
+ public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ private BackgroundDrawable mBackground;
+ private int mMarginStart;
+ private int mMarginBottom;
+ private int mWidth;
+ private int mHeight;
+ private int mMaxHeight;
+ private int mMinHeight;
+ private long mDuration;
+ private int mActionId;
+ private boolean mRemoveOnDismiss;
+
+ private Animation mInAnimation;
+ private Animation mOutAnimation;
+
+ private Runnable mDismissRunnable = new Runnable() {
+ @Override
+ public void run() {
+ dismiss();
+ }
+ };
+
+ private int mState = STATE_DISMISSED;
+
+ /**
+ * Indicate this SnackBar is already dismissed.
+ */
+ public static final int STATE_DISMISSED = 0;
+ /**
+ * Indicate this SnackBar is already shown.
+ */
+ public static final int STATE_SHOWN = 1;
+ /**
+ * Indicate this SnackBar is being shown.
+ */
+ public static final int STATE_SHOWING = 2;
+ /**
+ * Indicate this SnackBar is being dismissed.
+ */
+ public static final int STATE_DISMISSING = 3;
+
+ private boolean mIsRtl;
+
+ /**
+ * Interface definition for a callback to be invoked when action button is clicked.
+ */
+ public interface OnActionClickListener{
+
+ /**
+ * Called when action button is clicked.
+ * @param sb The SnackBar fire this event.
+ * @param actionId The ActionId of this SnackBar.
+ */
+ void onActionClick(SnackBar sb, int actionId);
+ }
+
+ private OnActionClickListener mActionClickListener;
+
+ /**
+ * Interface definition for a callback to be invoked when SnackBar's state is changed.
+ */
+ public interface OnStateChangeListener{
+
+ /**
+ * Called when SnackBar's state is changed.
+ * @param sb The SnackBar fire this event.
+ * @param oldState The old state of SnackBar.
+ * @param newState The new state of SnackBar.
+ */
+ void onStateChange(SnackBar sb, int oldState, int newState);
+ }
+
+ private OnStateChangeListener mStateChangeListener;
+
+ public static SnackBar make(Context context){
+ return new SnackBar(context);
+ }
+
+ public SnackBar(Context context){
+ super(context);
+ }
+
+ public SnackBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SnackBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public SnackBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ mWidth = MATCH_PARENT;
+ mHeight = WRAP_CONTENT;
+ mDuration = -1;
+ mIsRtl = false;
+
+ mText = new TextView(context);
+ mText.setSingleLine(true);
+ mText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ addView(mText, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+ mAction = new Button(context);
+ mAction.setBackgroundResource(0);
+ mAction.setGravity(Gravity.CENTER);
+ mAction.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mActionClickListener != null)
+ mActionClickListener.onActionClick(SnackBar.this, mActionId);
+
+ dismiss();
+ }
+
+ });
+ addView(mAction, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+ mBackground = new BackgroundDrawable();
+ mBackground.setColor(0xFF323232);
+ ViewUtil.setBackground(this, mBackground);
+ setClickable(true);
+
+ super.init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ super.applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackBar, defStyleAttr, defStyleRes);
+
+ int horizontalPadding = -1;
+ int verticalPadding = -1;
+ int textSize = -1;
+ int textColor = 0;
+ boolean textColorDefined = false;
+ int textAppearance = 0;
+ int actionTextSize = -1;
+ ColorStateList actionTextColor = null;
+ int actionTextAppearance = 0;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+ if(attr == R.styleable.SnackBar_sb_backgroundColor)
+ backgroundColor(a.getColor(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_backgroundCornerRadius)
+ backgroundRadius(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_horizontalPadding)
+ horizontalPadding = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.SnackBar_sb_verticalPadding)
+ verticalPadding = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.SnackBar_sb_width){
+ if(ThemeUtil.getType(a, attr) == TypedValue.TYPE_INT_DEC)
+ width(a.getInteger(attr, 0));
+ else
+ width(a.getDimensionPixelSize(attr, 0));
+ }
+ else if(attr == R.styleable.SnackBar_sb_height){
+ if(ThemeUtil.getType(a, attr) == TypedValue.TYPE_INT_DEC)
+ height(a.getInteger(attr, 0));
+ else
+ height(a.getDimensionPixelSize(attr, 0));
+ }
+ else if(attr == R.styleable.SnackBar_sb_minWidth)
+ minWidth(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_maxWidth)
+ maxWidth(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_minHeight)
+ minHeight(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_maxHeight)
+ maxHeight(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_marginStart)
+ marginStart(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_marginBottom)
+ marginBottom(a.getDimensionPixelSize(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_textSize)
+ textSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.SnackBar_sb_textColor) {
+ textColor = a.getColor(attr, 0);
+ textColorDefined = true;
+ }
+ else if(attr == R.styleable.SnackBar_sb_textAppearance)
+ textAppearance = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.SnackBar_sb_text)
+ text(a.getString(attr));
+ else if(attr == R.styleable.SnackBar_sb_singleLine)
+ singleLine(a.getBoolean(attr, true));
+ else if(attr == R.styleable.SnackBar_sb_maxLines)
+ maxLines(a.getInteger(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_lines)
+ lines(a.getInteger(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_ellipsize){
+ int ellipsize = a.getInteger(attr, 0);
+ switch (ellipsize) {
+ case 1:
+ ellipsize(TruncateAt.START);
+ break;
+ case 2:
+ ellipsize(TruncateAt.MIDDLE);
+ break;
+ case 3:
+ ellipsize(TruncateAt.END);
+ break;
+ case 4:
+ ellipsize(TruncateAt.MARQUEE);
+ break;
+ default:
+ ellipsize(TruncateAt.END);
+ break;
+ }
+ }
+ else if(attr == R.styleable.SnackBar_sb_actionTextSize)
+ actionTextSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.SnackBar_sb_actionTextColor)
+ actionTextColor = a.getColorStateList(attr);
+ else if(attr == R.styleable.SnackBar_sb_actionTextAppearance)
+ actionTextAppearance = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.SnackBar_sb_actionText)
+ actionText(a.getString(attr));
+ else if(attr == R.styleable.SnackBar_sb_actionRipple)
+ actionRipple(a.getResourceId(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_duration)
+ duration(a.getInteger(attr, 0));
+ else if(attr == R.styleable.SnackBar_sb_removeOnDismiss)
+ removeOnDismiss(a.getBoolean(attr, true));
+ else if(attr == R.styleable.SnackBar_sb_inAnimation)
+ animationIn(AnimationUtils.loadAnimation(getContext(), a.getResourceId(attr, 0)));
+ else if(attr == R.styleable.SnackBar_sb_outAnimation)
+ animationOut(AnimationUtils.loadAnimation(getContext(), a.getResourceId(attr, 0)));
+ }
+
+ a.recycle();
+
+ if(horizontalPadding >= 0 || verticalPadding >= 0)
+ padding(horizontalPadding >= 0 ? horizontalPadding : mText.getPaddingLeft(), verticalPadding >= 0 ? verticalPadding : mText.getPaddingTop());
+
+ if(textAppearance != 0)
+ textAppearance(textAppearance);
+ if(textSize >= 0)
+ textSize(textSize);
+ if(textColorDefined)
+ textColor(textColor);
+
+ if(textAppearance != 0)
+ actionTextAppearance(actionTextAppearance);
+ if(actionTextSize >= 0)
+ actionTextSize(actionTextSize);
+ if(actionTextColor != null)
+ actionTextColor(actionTextColor);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
+ if(mIsRtl != rtl) {
+ mIsRtl = rtl;
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
+ mText.setTextDirection((mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR));
+ mAction.setTextDirection((mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR));
+ }
+
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int width;
+ int height;
+
+ if(mAction.getVisibility() == View.VISIBLE){
+ mAction.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec);
+ int padding = mIsRtl ? mText.getPaddingLeft() : mText.getPaddingRight();
+ mText.measure(MeasureSpec.makeMeasureSpec(widthSize - (mAction.getMeasuredWidth() - padding), widthMode), heightMeasureSpec);
+ width = mText.getMeasuredWidth() + mAction.getMeasuredWidth() - padding;
+ }
+ else{
+ mText.measure(MeasureSpec.makeMeasureSpec(widthSize, widthMode), heightMeasureSpec);
+ width = mText.getMeasuredWidth();
+ }
+
+ height = Math.max(mText.getMeasuredHeight(), mAction.getMeasuredHeight());
+
+ switch (widthMode) {
+ case MeasureSpec.AT_MOST:
+ width = Math.min(widthSize, width);
+ break;
+ case MeasureSpec.EXACTLY:
+ width = widthSize;
+ break;
+ }
+
+ switch (heightMode) {
+ case MeasureSpec.AT_MOST:
+ height = Math.min(heightSize, height);
+ break;
+ case MeasureSpec.EXACTLY:
+ height = heightSize;
+ break;
+ }
+
+ if(mMaxHeight > 0)
+ height = Math.min(mMaxHeight, height);
+
+ if(mMinHeight > 0)
+ height = Math.max(mMinHeight, height);
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int childLeft = getPaddingLeft();
+ int childRight = r - l - getPaddingRight();
+ int childTop = getPaddingTop();
+ int childBottom = b - t - getPaddingBottom();
+
+ if(mAction.getVisibility() == View.VISIBLE){
+ if(mIsRtl) {
+ mAction.layout(childLeft, childTop, childLeft + mAction.getMeasuredWidth(), childBottom);
+ childLeft += mAction.getMeasuredWidth() - mText.getPaddingLeft();
+ }
+ else {
+ mAction.layout(childRight - mAction.getMeasuredWidth(), childTop, childRight, childBottom);
+ childRight -= mAction.getMeasuredWidth() - mText.getPaddingRight();
+ }
+ }
+
+ mText.layout(childLeft, childTop, childRight, childBottom);
+ }
+
+ /**
+ * Set the text that this SnackBar is to display.
+ * @param text The text is displayed.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar text(CharSequence text){
+ mText.setText(text);
+ return this;
+ }
+
+ /**
+ * Set the text that this SnackBar is to display.
+ * @param id The resourceId of text is displayed.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar text(int id){
+ return text(getContext().getResources().getString(id));
+ }
+
+ /**
+ * Set the text color.
+ * @param color The color of text.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar textColor(int color){
+ mText.setTextColor(color);
+ return this;
+ }
+
+ /**
+ * Set the text size to the given value, interpreted as "scaled pixel" units.
+ * @param size The size of text.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar textSize(float size){
+ mText.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
+ return this;
+ }
+
+ /**
+ * Sets the text color, size, style from the specified TextAppearance resource.
+ * @param resId The resourceId value.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar textAppearance(int resId){
+ if(resId != 0)
+ mText.setTextAppearance(getContext(), resId);
+ return this;
+ }
+
+ /**
+ * Causes words in the text that are longer than the view is wide to be ellipsized instead of broken in the middle.
+ * @param at a
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar ellipsize(TruncateAt at){
+ mText.setEllipsize(at);
+ return this;
+ }
+
+ /**
+ * Sets the text will be single-line or not.
+ * @param b f
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar singleLine(boolean b){
+ mText.setSingleLine(b);
+ return this;
+ }
+
+ /**
+ * Makes the text at most this many lines tall.
+ * @param lines The maximum line value.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar maxLines(int lines){
+ mText.setMaxLines(lines);
+ return this;
+ }
+
+ /**
+ * Makes the text exactly this many lines tall.
+ * @param lines The line number.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar lines(int lines){
+ mText.setLines(lines);
+ return this;
+ }
+
+ /**
+ * Set the actionId of this SnackBar. Used to determine the current action of this SnackBar.
+ * @param id The actionId value.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionId(int id){
+ mActionId = id;
+ return this;
+ }
+
+ /**
+ * Set the text that the ActionButton is to display.
+ * @param text If null, then the ActionButton will be hidden.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionText(CharSequence text){
+ if(TextUtils.isEmpty(text))
+ mAction.setVisibility(View.INVISIBLE);
+ else{
+ mAction.setVisibility(View.VISIBLE);
+ mAction.setText(text);
+ }
+ return this;
+ }
+
+ /**
+ * Set the text that the ActionButton is to display.
+ * @param id If 0, then the ActionButton will be hidden.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionText(int id){
+ if(id == 0)
+ return actionText(null);
+
+ return actionText(getContext().getResources().getString(id));
+ }
+
+ /**
+ * Set the text color of the ActionButton for all states.
+ * @param color The color of text.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionTextColor(int color){
+ mAction.setTextColor(color);
+ return this;
+ }
+
+ /**
+ * Set the text color of the ActionButton.
+ * @param colors c
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionTextColor(ColorStateList colors){
+ mAction.setTextColor(colors);
+ return this;
+ }
+
+ /**
+ * Sets the text color, size, style of the ActionButton from the specified TextAppearance resource.
+ * @param resId The resourceId value.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionTextAppearance(int resId){
+ if(resId != 0)
+ mAction.setTextAppearance(resId);
+ return this;
+ }
+
+ /**
+ * Set the text size of the ActionButton to the given value, interpreted as "scaled pixel" units.
+ * @param size The size of text.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionTextSize(float size){
+ mAction.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
+ return this;
+ }
+
+ /**
+ * Set the style of RippleEffect of the ActionButton.
+ * @param resId The resourceId of RippleEffect.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionRipple(int resId){
+ if(resId != 0)
+ ViewUtil.setBackground(mAction, new RippleDrawable.Builder(getContext(), resId).build());
+ return this;
+ }
+
+ /**
+ * Set the duration this SnackBar will be shown before dismissing.
+ * @param duration If 0, then the SnackBar will not be dismissed until {@link #dismiss() dismiss()} is called.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar duration(long duration){
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Set the background color of this SnackBar.
+ * @param color The color of background.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar backgroundColor(int color){
+ mBackground.setColor(color);
+ return this;
+ }
+
+ /**
+ * Set the background's corner radius of this SnackBar.
+ * @param radius The corner radius.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar backgroundRadius(int radius){
+ mBackground.setRadius(radius);
+ return this;
+ }
+
+ /**
+ * Set the horizontal padding between this SnackBar and it's text and button.
+ * @param padding p
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar horizontalPadding(int padding){
+ mText.setPadding(padding, mText.getPaddingTop(), padding, mText.getPaddingBottom());
+ mAction.setPadding(padding, mAction.getPaddingTop(), padding, mAction.getPaddingBottom());
+ return this;
+ }
+
+ /**
+ * Set the vertical padding between this SnackBar and it's text and button.
+ * @param padding p
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar verticalPadding(int padding){
+ mText.setPadding(mText.getPaddingLeft(), padding, mText.getPaddingRight(), padding);
+ mAction.setPadding(mAction.getPaddingLeft(), padding, mAction.getPaddingRight(), padding);
+ return this;
+ }
+
+ /**
+ * Set the padding between this SnackBar and it's text and button.
+ * @param horizontalPadding The horizontal padding.
+ * @param verticalPadding The vertical padding.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar padding(int horizontalPadding, int verticalPadding){
+ mText.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
+ mAction.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
+ return this;
+ }
+
+ /**
+ * Makes this SnackBar exactly this many pixels wide.
+ * @param width The width value in pixels.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar width(int width){
+ mWidth = width;
+ return this;
+ }
+
+ /**
+ * Makes this SnackBar at least this many pixels wide
+ * @param width The minimum width value in pixels.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar minWidth(int width){
+ mText.setMinWidth(width);
+ return this;
+ }
+
+ /**
+ * Makes this SnackBar at most this many pixels wide
+ * @param width The maximum width value in pixels.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar maxWidth(int width){
+ mText.setMaxWidth(width);
+ return this;
+ }
+
+ /**
+ * Makes this SnackBar exactly this many pixels tall.
+ * @param height The height value in pixels.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar height(int height){
+ mHeight = height;
+ return this;
+ }
+
+ /**
+ * Makes this SnackBar at most this many pixels tall
+ * @param height The maximum height value in pixels.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar maxHeight(int height){
+ mMaxHeight = height;
+ return this;
+ }
+
+ /**
+ * Makes this SnackBar at least this many pixels tall
+ * @param height The maximum height value in pixels.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar minHeight(int height){
+ mMinHeight = height;
+ return this;
+ }
+
+ /**
+ * Set the start margin between this SnackBar and it's parent.
+ * @param size s
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar marginStart(int size){
+ mMarginStart = size;
+ return this;
+ }
+
+ /**
+ * Set the bottom margin between this SnackBar and it's parent.
+ * @param size s
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar marginBottom(int size){
+ mMarginBottom = size;
+ return this;
+ }
+
+ /**
+ * Set the listener will be called when the ActionButton is clicked.
+ * @param listener The {@link OnActionClickListener} will be called.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar actionClickListener(OnActionClickListener listener){
+ mActionClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set the listener will be called when this SnackBar's state is changed.
+ * @param listener The {@link OnStateChangeListener} will be called.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar stateChangeListener(OnStateChangeListener listener){
+ mStateChangeListener = listener;
+ return this;
+ }
+
+ /**
+ * Set the animation will be shown when SnackBar enter screen.
+ * @param anim The animation.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar animationIn(Animation anim){
+ mInAnimation = anim;
+ return this;
+ }
+
+ /**
+ * Set the animation will be shown when SnackBar exit screen.
+ * @param anim The animation.
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar animationOut(Animation anim){
+ mOutAnimation = anim;
+ return this;
+ }
+
+ /**
+ * Indicate that this SnackBar should remove itself from parent view after being dismissed.
+ * @param b b
+ * @return This SnackBar for chaining methods.
+ */
+ public SnackBar removeOnDismiss(boolean b){
+ mRemoveOnDismiss = b;
+ return this;
+ }
+
+ /**
+ * Show this SnackBar. It will auto attach to the activity's root view.
+ */
+ public void show(Activity activity){
+ show((ViewGroup)activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT));
+ }
+
+ /**
+ * Show this SnackBar. It will auto attach to the parent view.
+ * @param parent Must be {@link android.widget.FrameLayout} or {@link android.widget.RelativeLayout}
+ */
+ public void show(ViewGroup parent){
+ if(mState == STATE_SHOWING || mState == STATE_DISMISSING)
+ return;
+
+ if(getParent() != parent) {
+ if(getParent() != null)
+ ((ViewGroup) getParent()).removeView(this);
+
+ parent.addView(this);
+ }
+
+ show();
+ }
+
+ /**
+ * Show this SnackBar.
+ * Make sure it already attached to a parent view or this method will do nothing.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public void show(){
+ ViewGroup parent = (ViewGroup)getParent();
+ if(parent == null || mState == STATE_SHOWING || mState == STATE_DISMISSING)
+ return;
+
+ if(parent instanceof android.widget.FrameLayout){
+ LayoutParams params = (LayoutParams)getLayoutParams();
+
+ params.width = mWidth;
+ params.height = mHeight;
+ params.gravity = Gravity.START | Gravity.BOTTOM;
+ if(mIsRtl)
+ params.rightMargin = mMarginStart;
+ else
+ params.leftMargin = mMarginStart;
+ params.bottomMargin = mMarginBottom;
+
+ setLayoutParams(params);
+ }
+ else if(parent instanceof android.widget.RelativeLayout){
+ android.widget.RelativeLayout.LayoutParams params = (android.widget.RelativeLayout.LayoutParams)getLayoutParams();
+
+ params.width = mWidth;
+ params.height = mHeight;
+ params.addRule(android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM);
+ params.addRule(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ? android.widget.RelativeLayout.ALIGN_PARENT_START : android.widget.RelativeLayout.ALIGN_PARENT_LEFT);
+ if(mIsRtl)
+ params.rightMargin = mMarginStart;
+ else
+ params.leftMargin = mMarginStart;
+ params.bottomMargin = mMarginBottom;
+
+ setLayoutParams(params);
+ }
+
+ if(mInAnimation != null && mState != STATE_SHOWN){
+ mInAnimation.cancel();
+ mInAnimation.reset();
+ mInAnimation.setAnimationListener(new Animation.AnimationListener() {
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ setState(STATE_SHOWING);
+ setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ setState(STATE_SHOWN);
+ startTimer();
+ }
+ });
+ clearAnimation();
+ startAnimation(mInAnimation);
+ }
+ else {
+ setVisibility(View.VISIBLE);
+ setState(STATE_SHOWN);
+ startTimer();
+ }
+ }
+
+ private void startTimer(){
+ removeCallbacks(mDismissRunnable);
+ if(mDuration > 0)
+ postDelayed(mDismissRunnable, mDuration);
+ }
+
+ /**
+ * Dismiss this SnackBar. It must be in {@link #STATE_SHOWN} to be dismissed.
+ */
+ public void dismiss(){
+ if(mState != STATE_SHOWN)
+ return;
+
+ removeCallbacks(mDismissRunnable);
+
+ if(mOutAnimation != null){
+ mOutAnimation.cancel();
+ mOutAnimation.reset();
+ mOutAnimation.setAnimationListener(new Animation.AnimationListener() {
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ setState(STATE_DISMISSING);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if(mRemoveOnDismiss && getParent() != null && getParent() instanceof ViewGroup)
+ ((ViewGroup)getParent()).removeView(SnackBar.this);
+
+ setState(STATE_DISMISSED);
+ setVisibility(View.GONE);
+ }
+ });
+ clearAnimation();
+ startAnimation(mOutAnimation);
+ }
+ else{
+ if(mRemoveOnDismiss && getParent() != null && getParent() instanceof ViewGroup)
+ ((ViewGroup)getParent()).removeView(this);
+
+ setState(STATE_DISMISSED);
+ setVisibility(View.GONE);
+ }
+
+ }
+
+ /**
+ * Get the current state of this SnackBar.
+ * @return The current state of this SnackBar. Can be {@link #STATE_DISMISSED}, {@link #STATE_DISMISSING}, {@link #STATE_SHOWING} or {@link #STATE_SHOWN}.
+ */
+ public int getState(){
+ return mState;
+ }
+
+ private void setState(int state){
+ if(mState != state){
+ int oldState = mState;
+ mState = state;
+ if(mStateChangeListener != null)
+ mStateChangeListener.onStateChange(this, oldState, mState);
+ }
+ }
+
+ private class BackgroundDrawable extends Drawable{
+
+ private int mBackgroundColor;
+ private int mBackgroundRadius;
+
+ private Paint mPaint;
+ private RectF mRect;
+
+ public BackgroundDrawable(){
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.FILL);
+
+ mRect = new RectF();
+ }
+
+ public void setColor(int color){
+ if(mBackgroundColor != color){
+ mBackgroundColor = color;
+ mPaint.setColor(mBackgroundColor);
+ invalidateSelf();
+ }
+ }
+
+ public void setRadius(int radius){
+ if(mBackgroundRadius != radius){
+ mBackgroundRadius = radius;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ mRect.set(bounds);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawRoundRect(mRect, mBackgroundRadius, mBackgroundRadius, mPaint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/Spinner.java b/material/src/main/java/com/rey/material/widget/Spinner.java
new file mode 100644
index 0000000..7c37afb
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/Spinner.java
@@ -0,0 +1,1294 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.SpinnerAdapter;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.ArrowDrawable;
+import com.rey.material.drawable.DividerDrawable;
+import com.rey.material.util.ThemeUtil;
+
+public class Spinner extends FrameLayout implements ThemeManager.OnThemeChangedListener{
+
+ private static final int MAX_ITEMS_MEASURED = 15;
+
+ private static final int INVALID_POSITION = -1;
+
+ /**
+ * Interface definition for a callback to be invoked when a item's view is clicked.
+ */
+ public interface OnItemClickListener{
+ /**
+ * Called when a item's view is clicked.
+ * @param parent The Spinner view.
+ * @param view The item view.
+ * @param position The position of item.
+ * @param id The id of item.
+ * @return false will make the Spinner doesn't select this item.
+ */
+ boolean onItemClick(Spinner parent, View view, int position, long id);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when an item is selected.
+ */
+ public interface OnItemSelectedListener{
+ /**
+ * Called when an item is selected.
+ * @param parent The Spinner view.
+ * @param view The item view.
+ * @param position The position of item.
+ * @param id The id of item.
+ */
+ void onItemSelected(Spinner parent, View view, int position, long id);
+ }
+
+ private boolean mLabelEnable;
+ private TextView mLabelView;
+
+ private SpinnerAdapter mAdapter;
+ private OnItemClickListener mOnItemClickListener;
+ private OnItemSelectedListener mOnItemSelectedListener;
+
+ private int mMinWidth;
+ private int mMinHeight;
+
+ private DropdownPopup mPopup;
+ private int mDropDownWidth;
+
+ private ArrowDrawable mArrowDrawable;
+ private int mArrowSize;
+ private int mArrowPadding;
+ private boolean mArrowAnimSwitchMode;
+
+ private DividerDrawable mDividerDrawable;
+ private int mDividerHeight;
+ private int mDividerPadding;
+
+ private int mGravity;
+ private boolean mDisableChildrenWhenDisabled;
+
+ private int mSelectedPosition;
+
+ private RecycleBin mRecycler = new RecycleBin();
+
+ private Rect mTempRect = new Rect();
+
+ private DropDownAdapter mTempAdapter;
+
+ private SpinnerDataSetObserver mDataSetObserver = new SpinnerDataSetObserver();
+
+ private boolean mIsRtl;
+
+ public Spinner(Context context) {
+ super(context, null, R.attr.listPopupWindowStyle);
+ }
+
+ public Spinner(Context context, AttributeSet attrs) {
+ super(context, attrs, R.attr.listPopupWindowStyle);
+ }
+
+ public Spinner(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ mLabelEnable = false;
+ mDropDownWidth = LayoutParams.WRAP_CONTENT;
+ mArrowAnimSwitchMode = false;
+ mGravity = Gravity.CENTER;
+ mDisableChildrenWhenDisabled = false;
+ mSelectedPosition = INVALID_POSITION;
+ mIsRtl = false;
+
+ setWillNotDraw(false);
+
+ mPopup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes);
+ mPopup.setModal(true);
+
+ if(isInEditMode())
+ applyStyle(R.style.Material_Widget_Spinner);
+
+ setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showPopup();
+ }
+ });
+
+ super.init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private android.widget.TextView getLabelView(){
+ if(mLabelView == null){
+ mLabelView = new TextView(getContext());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ mLabelView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR);
+ mLabelView.setSingleLine(true);
+ mLabelView.setDuplicateParentStateEnabled(true);
+ }
+
+ return mLabelView;
+ }
+
+ @Override
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ super.applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ removeAllViews();
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
+
+ int arrowAnimDuration = -1;
+ ColorStateList arrowColor = null;
+ Interpolator arrowInterpolator = null;
+ boolean arrowClockwise = true;
+ int dividerAnimDuration = -1;
+ ColorStateList dividerColor = null;
+ ColorStateList labelTextColor = null;
+ int labelTextSize = -1;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+
+ if(attr == R.styleable.Spinner_spn_labelEnable)
+ mLabelEnable = a.getBoolean(attr, false);
+ else if(attr == R.styleable.Spinner_spn_labelPadding)
+ getLabelView().setPadding(0, 0, 0, a.getDimensionPixelSize(attr, 0));
+ else if (attr == R.styleable.Spinner_spn_labelTextSize)
+ labelTextSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_labelTextColor)
+ labelTextColor = a.getColorStateList(attr);
+ else if(attr == R.styleable.Spinner_spn_labelTextAppearance)
+ getLabelView().setTextAppearance(context, a.getResourceId(attr, 0));
+ else if(attr == R.styleable.Spinner_spn_labelEllipsize){
+ int labelEllipsize = a.getInteger(attr, 0);
+ switch (labelEllipsize) {
+ case 1:
+ getLabelView().setEllipsize(TextUtils.TruncateAt.START);
+ break;
+ case 2:
+ getLabelView().setEllipsize(TextUtils.TruncateAt.MIDDLE);
+ break;
+ case 3:
+ getLabelView().setEllipsize(TextUtils.TruncateAt.END);
+ break;
+ case 4:
+ getLabelView().setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ break;
+ default:
+ getLabelView().setEllipsize(TextUtils.TruncateAt.END);
+ break;
+ }
+ }
+ else if(attr == R.styleable.Spinner_spn_label)
+ getLabelView().setText(a.getString(attr));
+ else if(attr == R.styleable.Spinner_android_gravity)
+ mGravity = a.getInt(attr, 0);
+ else if(attr == R.styleable.Spinner_android_minWidth)
+ setMinimumWidth(a.getDimensionPixelOffset(attr, 0));
+ else if(attr == R.styleable.Spinner_android_minHeight)
+ setMinimumHeight(a.getDimensionPixelOffset(attr, 0));
+ else if(attr == R.styleable.Spinner_android_dropDownWidth)
+ mDropDownWidth = a.getLayoutDimension(attr, LayoutParams.WRAP_CONTENT);
+ else if(attr == R.styleable.Spinner_android_popupBackground)
+ mPopup.setBackgroundDrawable(a.getDrawable(attr));
+ else if(attr == R.styleable.Spinner_android_prompt)
+ mPopup.setPromptText(a.getString(attr));
+ else if(attr == R.styleable.Spinner_spn_popupItemAnimation)
+ mPopup.setItemAnimation(a.getResourceId(attr, 0));
+ else if(attr == R.styleable.Spinner_spn_popupItemAnimOffset)
+ mPopup.setItemAnimationOffset(a.getInteger(attr, 0));
+ else if(attr == R.styleable.Spinner_spn_disableChildrenWhenDisabled)
+ mDisableChildrenWhenDisabled = a.getBoolean(attr, false);
+ else if(attr == R.styleable.Spinner_spn_arrowSwitchMode)
+ mArrowAnimSwitchMode = a.getBoolean(attr, false);
+ else if(attr == R.styleable.Spinner_spn_arrowAnimDuration)
+ arrowAnimDuration = a.getInteger(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_arrowSize)
+ mArrowSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_arrowPadding)
+ mArrowPadding = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_arrowColor)
+ arrowColor = a.getColorStateList(attr);
+ else if(attr == R.styleable.Spinner_spn_arrowInterpolator){
+ int resId = a.getResourceId(attr, 0);
+ arrowInterpolator = AnimationUtils.loadInterpolator(context, resId);
+ }
+ else if(attr == R.styleable.Spinner_spn_arrowAnimClockwise)
+ arrowClockwise = a.getBoolean(attr, true);
+ else if(attr == R.styleable.Spinner_spn_dividerHeight)
+ mDividerHeight = a.getDimensionPixelOffset(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_dividerPadding)
+ mDividerPadding = a.getDimensionPixelOffset(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_dividerAnimDuration)
+ dividerAnimDuration = a.getInteger(attr, 0);
+ else if(attr == R.styleable.Spinner_spn_dividerColor)
+ dividerColor = a.getColorStateList(attr);
+ }
+
+ a.recycle();
+
+ if(labelTextColor != null)
+ getLabelView().setTextColor(labelTextColor);
+
+ if(labelTextSize >= 0)
+ getLabelView().setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
+
+ if(mLabelEnable)
+ addView(getLabelView(), 0, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ if(mArrowSize > 0){
+ if(mArrowDrawable == null){
+ if(arrowColor == null)
+ arrowColor = ColorStateList.valueOf(ThemeUtil.colorControlNormal(context, 0xFF000000));
+
+ if(arrowAnimDuration < 0)
+ arrowAnimDuration = 0;
+
+ mArrowDrawable = new ArrowDrawable(ArrowDrawable.MODE_DOWN, mArrowSize, arrowColor, arrowAnimDuration, arrowInterpolator, arrowClockwise);
+ mArrowDrawable.setCallback(this);
+ }
+ else{
+ mArrowDrawable.setArrowSize(mArrowSize);
+ mArrowDrawable.setClockwise(arrowClockwise);
+
+ if(arrowColor != null)
+ mArrowDrawable.setColor(arrowColor);
+
+ if(arrowAnimDuration >= 0)
+ mArrowDrawable.setAnimationDuration(arrowAnimDuration);
+
+ if(arrowInterpolator != null)
+ mArrowDrawable.setInterpolator(arrowInterpolator);
+ }
+ }
+ else if(mArrowDrawable != null){
+ mArrowDrawable.setCallback(null);
+ mArrowDrawable = null;
+ }
+
+ if(mDividerHeight > 0){
+ if(mDividerDrawable == null){
+ if(dividerAnimDuration < 0)
+ dividerAnimDuration = 0;
+
+ if(dividerColor == null){
+ int[][] states = new int[][]{
+ new int[]{-android.R.attr.state_pressed},
+ new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled},
+ };
+ int[] colors = new int[]{
+ ThemeUtil.colorControlNormal(context, 0xFF000000),
+ ThemeUtil.colorControlActivated(context, 0xFF000000),
+ };
+
+ dividerColor = new ColorStateList(states, colors);
+ }
+
+ mDividerDrawable = new DividerDrawable(mDividerHeight, dividerColor, dividerAnimDuration);
+ mDividerDrawable.setCallback(this);
+ }
+ else{
+ mDividerDrawable.setDividerHeight(mDividerHeight);
+
+ if(dividerColor != null)
+ mDividerDrawable.setColor(dividerColor);
+
+ if(dividerAnimDuration >= 0)
+ mDividerDrawable.setAnimationDuration(dividerAnimDuration);
+ }
+ }
+ else if(mDividerDrawable != null){
+ mDividerDrawable.setCallback(null);
+ mDividerDrawable = null;
+ }
+
+ if (mTempAdapter != null) {
+ mPopup.setAdapter(mTempAdapter);
+ mTempAdapter = null;
+ }
+
+ if(mAdapter != null)
+ setAdapter(mAdapter);
+
+ if(isInEditMode()){
+ TextView tv = new TextView(context, attrs, defStyleAttr);
+ tv.setText("Item 1");
+ super.addView(tv);
+ }
+
+ requestLayout();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
+ if(mIsRtl != rtl) {
+ mIsRtl = rtl;
+
+ if(mLabelView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ mLabelView.setTextDirection(mIsRtl ? TEXT_DIRECTION_RTL : TEXT_DIRECTION_LTR);
+
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return The selected item's view.
+ */
+ public View getSelectedView() {
+ View v = getChildAt(getChildCount() - 1);
+ return v == mLabelView ? null : v;
+ }
+
+ /**
+ * Set the selected position of this Spinner.
+ * @param position The selected position.
+ */
+ public void setSelection(int position) {
+ if(mAdapter != null)
+ position = Math.max(0, Math.min(position, mAdapter.getCount() - 1));
+
+ if(mSelectedPosition != position){
+ mSelectedPosition = position;
+
+ if(mOnItemSelectedListener != null)
+ mOnItemSelectedListener.onItemSelected(this, getSelectedView(), position, mAdapter == null ? -1 : mAdapter.getItemId(position));
+
+ onDataInvalidated();
+ }
+ }
+
+ /**
+ * @return The selected posiiton.
+ */
+ public int getSelectedItemPosition(){
+ return mSelectedPosition;
+ }
+
+ /**
+ * @return The selected item.
+ */
+ public Object getSelectedItem(){
+ return mAdapter == null ? null : mAdapter.getItem(mSelectedPosition);
+ }
+
+ /**
+ * @return The adapter back this Spinner.
+ */
+ public SpinnerAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Set an adapter for this Spinner.
+ * @param adapter
+ */
+ public void setAdapter(SpinnerAdapter adapter) {
+ if(mAdapter != null)
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+
+ mRecycler.clear();
+
+ mAdapter = adapter;
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+ onDataChanged();
+
+ if (mPopup != null)
+ mPopup.setAdapter(new DropDownAdapter(adapter));
+ else
+ mTempAdapter = new DropDownAdapter(adapter);
+ }
+
+ /**
+ * Set the background drawable for the spinner's popup window of choices.
+ *
+ * @param background Background drawable
+ *
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ */
+ public void setPopupBackgroundDrawable(Drawable background) {
+ mPopup.setBackgroundDrawable(background);
+ }
+
+ /**
+ * Set the background drawable for the spinner's popup window of choices.
+ *
+ * @param resId Resource ID of a background drawable
+ *
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ */
+ public void setPopupBackgroundResource(int resId) {
+ setPopupBackgroundDrawable(getContext().getDrawable(resId));
+ }
+
+ /**
+ * Get the background drawable for the spinner's popup window of choices.
+ *
+ * @return background Background drawable
+ *
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ */
+ public Drawable getPopupBackground() {
+ return mPopup.getBackground();
+ }
+
+ /**
+ * Set a vertical offset in pixels for the spinner's popup window of choices.
+ *
+ * @param pixels Vertical offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public void setDropDownVerticalOffset(int pixels) {
+ mPopup.setVerticalOffset(pixels);
+ }
+
+ /**
+ * Get the configured vertical offset in pixels for the spinner's popup window of choices.
+ *
+ * @return Vertical offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ */
+ public int getDropDownVerticalOffset() {
+ return mPopup.getVerticalOffset();
+ }
+
+ /**
+ * Set a horizontal offset in pixels for the spinner's popup window of choices.
+ *
+ * @param pixels Horizontal offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public void setDropDownHorizontalOffset(int pixels) {
+ mPopup.setHorizontalOffset(pixels);
+ }
+
+ /**
+ * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
+ *
+ * @return Horizontal offset in pixels
+ *
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+ public int getDropDownHorizontalOffset() {
+ return mPopup.getHorizontalOffset();
+ }
+
+ /**
+ * Set the width of the spinner's popup window of choices in pixels. This value
+ * may also be set to {@link ViewGroup.LayoutParams#MATCH_PARENT}
+ * to match the width of the Spinner itself, or
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
+ * of contained dropdown list items.
+ *
+ * @param pixels Width in pixels, WRAP_CONTENT, or MATCH_PARENT
+ *
+ * @attr ref android.R.styleable#Spinner_dropDownWidth
+ */
+ public void setDropDownWidth(int pixels) {
+ mDropDownWidth = pixels;
+ }
+
+ /**
+ * Get the configured width of the spinner's popup window of choices in pixels.
+ * The returned value may also be {@link ViewGroup.LayoutParams#MATCH_PARENT}
+ * meaning the popup window will match the width of the Spinner itself, or
+ * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
+ * of contained dropdown list items.
+ *
+ * @return Width in pixels, WRAP_CONTENT, or MATCH_PARENT
+ *
+ * @attr ref android.R.styleable#Spinner_dropDownWidth
+ */
+ public int getDropDownWidth() {
+ return mDropDownWidth;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (mDisableChildrenWhenDisabled) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++)
+ getChildAt(i).setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void setMinimumHeight(int minHeight) {
+ mMinHeight = minHeight;
+ super.setMinimumHeight(minHeight);
+ }
+
+ @Override
+ public void setMinimumWidth(int minWidth) {
+ mMinWidth = minWidth;
+ super.setMinimumWidth(minWidth);
+ }
+
+ /**
+ * Describes how the selected item view is positioned.
+ *
+ * @param gravity See {@link Gravity}
+ *
+ * @attr ref android.R.styleable#Spinner_gravity
+ */
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0)
+ gravity |= Gravity.START;
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ @Override
+ public int getBaseline() {
+ View child = getSelectedView();
+
+ if (child != null) {
+ final int childBaseline = child.getBaseline();
+ return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
+ }
+
+ return -1;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mPopup != null && mPopup.isShowing())
+ mPopup.dismiss();
+ }
+
+ /**
+ * Set a listener that will be called when a item's view is clicked.
+ * @param l The {@link OnItemClickListener} will be called.
+ */
+ public void setOnItemClickListener(OnItemClickListener l) {
+ mOnItemClickListener = l;
+ }
+
+ /**
+ * Set a listener that will be called when an item is selected.
+ * @param l The {@link OnItemSelectedListener} will be called.
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener l) {
+ mOnItemSelectedListener = l;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return true;
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || mArrowDrawable == who || mDividerDrawable == who;
+ }
+
+ private int getArrowDrawableWidth(){
+ return mArrowDrawable != null ? mArrowSize + mArrowPadding * 2 : 0;
+ }
+
+ private int getDividerDrawableHeight(){
+ return mDividerHeight > 0 ? mDividerHeight + mDividerPadding : 0;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int paddingHorizontal = getPaddingLeft() + getPaddingRight() + getArrowDrawableWidth();
+ int paddingVertical = getPaddingTop() + getPaddingBottom() + getDividerDrawableHeight();
+
+ int labelWidth = 0;
+ int labelHeight = 0;
+ if(mLabelView != null && mLabelView.getLayoutParams() != null){
+ mLabelView.measure(MeasureSpec.makeMeasureSpec(widthSize - paddingHorizontal, widthMode), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ labelWidth = mLabelView.getMeasuredWidth();
+ labelHeight = mLabelView.getMeasuredHeight();
+ }
+
+ int width = 0;
+ int height = 0;
+
+ View v = getSelectedView();
+ if(v != null){
+ int ws;
+ int hs;
+ ViewGroup.LayoutParams params = v.getLayoutParams();
+ switch (params.width){
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ ws = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ break;
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ ws = MeasureSpec.makeMeasureSpec(widthSize - paddingHorizontal, widthMode);
+ break;
+ default:
+ ws = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY);
+ break;
+ }
+ switch (params.height){
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ hs = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ break;
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ hs = MeasureSpec.makeMeasureSpec(heightSize - paddingVertical - labelHeight, heightMode);
+ break;
+ default:
+ hs = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY);
+ break;
+ }
+
+ v.measure(ws, hs);
+ width = v.getMeasuredWidth();
+ height = v.getMeasuredHeight();
+ }
+
+ width = Math.max(mMinWidth, Math.max(labelWidth, width) + paddingHorizontal);
+ height = Math.max(mMinHeight, height + labelHeight + paddingVertical);
+
+ switch (widthMode){
+ case MeasureSpec.AT_MOST:
+ width = Math.min(widthSize, width);
+ break;
+ case MeasureSpec.EXACTLY:
+ width = widthSize;
+ break;
+ }
+
+ switch (heightMode){
+ case MeasureSpec.AT_MOST:
+ height = Math.min(heightSize, height);
+ break;
+ case MeasureSpec.EXACTLY:
+ height = heightSize;
+ break;
+ }
+
+ setMeasuredDimension(width, height);
+
+ if(v != null){
+ ViewGroup.LayoutParams params = v.getLayoutParams();
+ int viewWidth;
+ int viewHeight;
+ switch (params.width){
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ viewWidth = v.getMeasuredWidth();
+ break;
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ viewWidth = width - paddingHorizontal;
+ break;
+ default:
+ viewWidth = params.width;
+ break;
+ }
+ switch (params.height){
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ viewHeight = v.getMeasuredHeight();
+ break;
+ case ViewGroup.LayoutParams.MATCH_PARENT:
+ viewHeight = height - labelHeight - paddingVertical;
+ break;
+ default:
+ viewHeight = params.height;
+ break;
+ }
+
+ if(v.getMeasuredWidth() != viewWidth || v.getMeasuredHeight() != viewHeight)
+ v.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int w = r - l;
+ int h = b - t;
+ int arrowWidth = getArrowDrawableWidth();
+
+ if(mArrowDrawable != null) {
+ int top = getPaddingTop() + (mLabelView == null ? 0 : mLabelView.getMeasuredHeight());
+ int bottom = h - getDividerDrawableHeight() - getPaddingBottom();
+ if(mIsRtl)
+ mArrowDrawable.setBounds(getPaddingLeft(), top, getPaddingLeft() + arrowWidth, bottom);
+ else
+ mArrowDrawable.setBounds(getWidth() - getPaddingRight() - arrowWidth, top, getWidth() - getPaddingRight(), bottom);
+ }
+
+ if(mDividerDrawable != null)
+ mDividerDrawable.setBounds(getPaddingLeft(), h - mDividerHeight - getPaddingBottom(), w - getPaddingRight(), h - getPaddingBottom());
+
+ int childLeft = mIsRtl ? (getPaddingLeft() + arrowWidth) : getPaddingLeft();
+ int childRight = mIsRtl ? (w - getPaddingRight()) : (w - getPaddingRight() - arrowWidth);
+ int childTop = getPaddingTop();
+ int childBottom = h - getPaddingBottom();
+
+ if(mLabelView != null){
+ if(mIsRtl)
+ mLabelView.layout(childRight - mLabelView.getMeasuredWidth(), childTop, childRight, childTop + mLabelView.getMeasuredHeight());
+ else
+ mLabelView.layout(childLeft, childTop, childLeft + mLabelView.getMeasuredWidth(), childTop + mLabelView.getMeasuredHeight());
+ childTop += mLabelView.getMeasuredHeight();
+ }
+
+ View v = getSelectedView();
+ if(v != null){
+ int x, y;
+
+ int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if(horizontalGravity == Gravity.START)
+ horizontalGravity = mIsRtl ? Gravity.RIGHT : Gravity.LEFT;
+ else if(horizontalGravity == Gravity.END)
+ horizontalGravity = mIsRtl ? Gravity.LEFT : Gravity.RIGHT;
+
+ switch (horizontalGravity) {
+ case Gravity.LEFT:
+ x = childLeft;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ x = (childRight - childLeft - v.getMeasuredWidth()) / 2 + childLeft;
+ break;
+ case Gravity.RIGHT:
+ x = childRight - v.getMeasuredWidth();
+ break;
+ default:
+ x = (childRight - childLeft - v.getMeasuredWidth()) / 2 + childLeft;
+ break;
+ }
+
+ int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (verticalGravity) {
+ case Gravity.TOP:
+ y = childTop;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ y = (childBottom - childTop - v.getMeasuredHeight()) / 2 + childTop;
+ break;
+ case Gravity.BOTTOM:
+ y = childBottom - v.getMeasuredHeight();
+ break;
+ default:
+ y = (childBottom - childTop - v.getMeasuredHeight()) / 2 + childTop;
+ break;
+ }
+
+ v.layout(x, y, x + v.getMeasuredWidth(), y + v.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+ if(mDividerDrawable != null)
+ mDividerDrawable.draw(canvas);
+ if(mArrowDrawable != null)
+ mArrowDrawable.draw(canvas);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if(mArrowDrawable != null)
+ mArrowDrawable.setState(getDrawableState());
+ if(mDividerDrawable != null)
+ mDividerDrawable.setState(getDrawableState());
+ }
+
+ public boolean performItemClick(View view, int position, long id) {
+ if (mOnItemClickListener != null) {
+// playSoundEffect(SoundEffectConstants.CLICK);
+// if (view != null)
+// view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+ if(mOnItemClickListener.onItemClick(this, view, position, id))
+ setSelection(position);
+
+ return true;
+ }
+ else
+ setSelection(position);
+
+ return false;
+ }
+
+ private void onDataChanged(){
+ if(mSelectedPosition == INVALID_POSITION)
+ setSelection(0);
+ else if(mSelectedPosition < mAdapter.getCount())
+ onDataInvalidated();
+ else
+ setSelection(mAdapter.getCount() - 1);
+ }
+
+ private void onDataInvalidated(){
+ if(mAdapter == null)
+ return;
+
+ if(mLabelView == null)
+ removeAllViews();
+ else
+ for(int i = getChildCount() - 1; i > 0; i--)
+ removeViewAt(i);
+
+ int type = mAdapter.getItemViewType(mSelectedPosition);
+ View v = mAdapter.getView(mSelectedPosition, mRecycler.get(type), this);
+ v.setFocusable(false);
+ v.setClickable(false);
+
+ if(v.getParent() != null)
+ ((ViewGroup)v.getParent()).removeView(v);
+
+ super.addView(v);
+
+ mRecycler.put(type, v);
+ }
+
+ private void showPopup(){
+ if (!mPopup.isShowing()){
+ mPopup.show();
+ final ListView lv = mPopup.getListView();
+ if(lv != null){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ lv.setSelection(getSelectedItemPosition());
+ if(mArrowDrawable != null && mArrowAnimSwitchMode)
+ lv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ lv.getViewTreeObserver().removeOnPreDrawListener(this);
+ mArrowDrawable.setMode(ArrowDrawable.MODE_UP, true);
+ return true;
+ }
+ });
+ }
+
+ }
+ }
+
+ private void onPopupDismissed(){
+ if(mArrowDrawable != null)
+ mArrowDrawable.setMode(ArrowDrawable.MODE_DOWN, true);
+ }
+
+ private int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
+ if (adapter == null)
+ return 0;
+
+ int width = 0;
+ View itemView = null;
+ int itemType = 0;
+
+ final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+
+ // Make sure the number of items we'll measure is capped. If it's a huge data set
+ // with wildly varying sizes, oh well.
+ int start = Math.max(0, getSelectedItemPosition());
+ final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
+ final int count = end - start;
+ start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
+ for (int i = start; i < end; i++) {
+ final int positionType = adapter.getItemViewType(i);
+ if (positionType != itemType) {
+ itemType = positionType;
+ itemView = null;
+ }
+ itemView = adapter.getView(i, itemView, null);
+ if (itemView.getLayoutParams() == null)
+ itemView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+ itemView.measure(widthMeasureSpec, heightMeasureSpec);
+ width = Math.max(width, itemView.getMeasuredWidth());
+ }
+
+ // Add background padding to measured width
+ if (background != null) {
+ background.getPadding(mTempRect);
+ width += mTempRect.left + mTempRect.right;
+ }
+
+ return width;
+ }
+
+ static class SavedState extends BaseSavedState {
+
+ int position;
+ boolean showDropdown;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ SavedState(Parcel in) {
+ super(in);
+ position = in.readInt();
+ showDropdown = in.readByte() != 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(position);
+ out.writeByte((byte) (showDropdown ? 1 : 0));
+ }
+
+ @Override
+ public String toString() {
+ return "AbsSpinner.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " position=" + position
+ + " showDropdown=" + showDropdown + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.position = getSelectedItemPosition();
+ ss.showDropdown = mPopup != null && mPopup.isShowing();
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ setSelection(ss.position);
+
+ if (ss.showDropdown) {
+ ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null) {
+ final ViewTreeObserver.OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ showPopup();
+ final ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null)
+ vto.removeGlobalOnLayoutListener(this);
+ }
+ };
+ vto.addOnGlobalLayoutListener(listener);
+ }
+ }
+ }
+
+ private class SpinnerDataSetObserver extends DataSetObserver{
+
+ @Override
+ public void onChanged() {
+ onDataChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ onDataInvalidated();
+ }
+
+ }
+
+ private class RecycleBin {
+ private final SparseArray mScrapHeap = new SparseArray<>();
+
+ public void put(int type, View v) {
+ mScrapHeap.put(type, v);
+ }
+
+ View get(int type) {
+ View result = mScrapHeap.get(type);
+ if (result != null)
+ mScrapHeap.delete(type);
+
+ return result;
+ }
+
+ void clear() {
+ final SparseArray scrapHeap = mScrapHeap;
+ scrapHeap.clear();
+ }
+ }
+
+ private static class DropDownAdapter implements ListAdapter, SpinnerAdapter, OnClickListener {
+
+ private SpinnerAdapter mAdapter;
+
+ private ListAdapter mListAdapter;
+
+ private AdapterView.OnItemClickListener mOnItemClickListener;
+
+ /**
+ * Creates a new ListAdapter wrapper for the specified adapter.
+ *
+ * @param adapter the Adapter to transform into a ListAdapter
+ */
+ public DropDownAdapter(SpinnerAdapter adapter) {
+ this.mAdapter = adapter;
+ if (adapter instanceof ListAdapter)
+ this.mListAdapter = (ListAdapter) adapter;
+ }
+
+ public void setOnItemClickListener(AdapterView.OnItemClickListener listener){
+ mOnItemClickListener = listener;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int position = (Integer) v.getTag();
+ if(mOnItemClickListener != null)
+ mOnItemClickListener.onItemClick(null, v, position, 0);
+ }
+
+ public int getCount() {
+ return mAdapter == null ? 0 : mAdapter.getCount();
+ }
+
+ public Object getItem(int position) {
+ return mAdapter == null ? null : mAdapter.getItem(position);
+ }
+
+ public long getItemId(int position) {
+ return mAdapter == null ? -1 : mAdapter.getItemId(position);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = getDropDownView(position, convertView, parent);
+ v.setOnClickListener(this);
+ v.setTag(position);
+ return v;
+ }
+
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return (mAdapter == null) ? null : mAdapter.getDropDownView(position, convertView, parent);
+ }
+
+ public boolean hasStableIds() {
+ return mAdapter != null && mAdapter.hasStableIds();
+ }
+
+ /**
+ * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. Otherwise,
+ * return true.
+ */
+ public boolean areAllItemsEnabled() {
+ final ListAdapter adapter = mListAdapter;
+ return adapter == null || adapter.areAllItemsEnabled();
+ }
+
+ /**
+ * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. Otherwise,
+ * return true.
+ */
+ public boolean isEnabled(int position) {
+ final ListAdapter adapter = mListAdapter;
+ return adapter == null || adapter.isEnabled(position);
+ }
+
+ public int getItemViewType(int position) {
+ final ListAdapter adapter = mListAdapter;
+ if (adapter != null)
+ return adapter.getItemViewType(position);
+ else
+ return 0;
+ }
+
+ public int getViewTypeCount() {
+ final ListAdapter adapter = mListAdapter;
+ if (adapter != null)
+ return adapter.getViewTypeCount();
+ else
+ return 1;
+ }
+
+ public boolean isEmpty() {
+ return getCount() == 0;
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null)
+ mAdapter.registerDataSetObserver(observer);
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null)
+ mAdapter.unregisterDataSetObserver(observer);
+ }
+ }
+
+ private class DropdownPopup extends ListPopupWindow {
+
+ private CharSequence mHintText;
+
+ private DropDownAdapter mAdapter;
+
+ private ViewTreeObserver.OnGlobalLayoutListener layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ computeContentWidth();
+
+ // Use super.show here to update; we don't want to move the selected
+ // position or adjust other things that would be reset otherwise.
+ DropdownPopup.super.show();
+ }
+ };
+
+ public DropdownPopup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setAnchorView(Spinner.this);
+ setModal(true);
+ setPromptPosition(POSITION_PROMPT_ABOVE);
+
+ setOnDismissListener(new PopupWindow.OnDismissListener() {
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public void onDismiss() {
+ final ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null) {
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ vto.removeOnGlobalLayoutListener(layoutListener);
+ else
+ vto.removeGlobalOnLayoutListener(layoutListener);
+ }
+ onPopupDismissed();
+ }
+
+ });
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ super.setAdapter(adapter);
+ mAdapter = (DropDownAdapter)adapter;
+ mAdapter.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ Spinner.this.performItemClick(v, position, mAdapter.getItemId(position));
+ dismiss();
+ }
+ });
+ }
+
+ public CharSequence getHintText() {
+ return mHintText;
+ }
+
+ public void setPromptText(CharSequence hintText) {
+ mHintText = hintText;
+ }
+
+ void computeContentWidth() {
+ final Drawable background = getBackground();
+ int hOffset = 0;
+ if (background != null) {
+ background.getPadding(mTempRect);
+ hOffset = mIsRtl ? mTempRect.right : -mTempRect.left;
+ } else
+ mTempRect.left = mTempRect.right = 0;
+
+ final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
+ final int spinnerPaddingRight = Spinner.this.getPaddingRight();
+ final int spinnerWidth = Spinner.this.getWidth();
+
+ if (mDropDownWidth == WRAP_CONTENT) {
+ int contentWidth = measureContentWidth((SpinnerAdapter) mAdapter, getBackground());
+ final int contentWidthLimit = getContext().getResources().getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
+ if (contentWidth > contentWidthLimit)
+ contentWidth = contentWidthLimit;
+
+ setContentWidth(Math.max(contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
+ } else if (mDropDownWidth == MATCH_PARENT)
+ setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
+ else
+ setContentWidth(mDropDownWidth);
+
+ if (mIsRtl)
+ hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
+ else
+ hOffset += spinnerPaddingLeft;
+
+ setHorizontalOffset(hOffset);
+ }
+
+ public void show() {
+ final boolean wasShowing = isShowing();
+
+ computeContentWidth();
+ setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+ super.show();
+
+ if (wasShowing) {
+ // Skip setting up the layout/dismiss listener below. If we were previously
+ // showing it will still stick around.
+ return;
+ }
+
+ // Make sure we hide if our anchor goes away.
+ // TODO: This might be appropriate to push all the way down to PopupWindow,
+ // but it may have other side effects to investigate first. (Text editing handles, etc.)
+ final ViewTreeObserver vto = getViewTreeObserver();
+ if (vto != null)
+ vto.addOnGlobalLayoutListener(layoutListener);
+ }
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/Switch.java b/material/src/main/java/com/rey/material/widget/Switch.java
new file mode 100644
index 0000000..ca63ce8
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/Switch.java
@@ -0,0 +1,690 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.Checkable;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.ColorUtil;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.ViewUtil;
+
+public class Switch extends View implements Checkable, ThemeManager.OnThemeChangedListener {
+
+ private RippleManager mRippleManager;
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ private boolean mRunning = false;
+
+ private Paint mPaint;
+ private RectF mDrawRect;
+ private RectF mTempRect;
+ private Path mTrackPath;
+
+ private int mTrackSize = -1;
+ private ColorStateList mTrackColors;
+ private Paint.Cap mTrackCap = Paint.Cap.ROUND;
+ private int mThumbRadius = -1;
+ private ColorStateList mThumbColors;
+ private float mThumbPosition;
+ private int mMaxAnimDuration = -1;
+ private Interpolator mInterpolator;
+ private int mGravity = Gravity.CENTER_VERTICAL;
+
+ private boolean mChecked = false;
+ private float mMemoX;
+
+ private float mStartX;
+ private float mFlingVelocity;
+
+ private long mStartTime;
+ private int mAnimDuration;
+ private float mStartPosition;
+
+ private int[] mTempStates = new int[2];
+
+ private int mShadowSize = -1;
+ private int mShadowOffset = -1;
+ private Path mShadowPath;
+ private Paint mShadowPaint;
+
+ private static final int COLOR_SHADOW_START = 0x4C000000;
+ private static final int COLOR_SHADOW_END = 0x00000000;
+
+ private boolean mIsRtl = false;
+
+ /**
+ * Interface definition for a callback to be invoked when the checked state is changed.
+ */
+ public interface OnCheckedChangeListener {
+ /**
+ * Called when the checked state is changed.
+ *
+ * @param view The Switch view.
+ * @param checked The checked state.
+ */
+ void onCheckedChanged(Switch view, boolean checked);
+ }
+
+ private OnCheckedChangeListener mOnCheckedChangeListener;
+
+ public Switch(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public Switch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ mDrawRect = new RectF();
+ mTempRect = new RectF();
+ mTrackPath = new Path();
+
+ mFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ if (!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId) {
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Switch, defStyleAttr, defStyleRes);
+
+ for (int i = 0, count = a.getIndexCount(); i < count; i++) {
+ int attr = a.getIndex(i);
+ if (attr == R.styleable.Switch_sw_trackSize)
+ mTrackSize = a.getDimensionPixelSize(attr, 0);
+ else if (attr == R.styleable.Switch_sw_trackColor)
+ mTrackColors = a.getColorStateList(attr);
+ else if (attr == R.styleable.Switch_sw_trackCap) {
+ int cap = a.getInteger(attr, 0);
+ if (cap == 0)
+ mTrackCap = Paint.Cap.BUTT;
+ else if (cap == 1)
+ mTrackCap = Paint.Cap.ROUND;
+ else
+ mTrackCap = Paint.Cap.SQUARE;
+ } else if (attr == R.styleable.Switch_sw_thumbColor)
+ mThumbColors = a.getColorStateList(attr);
+ else if (attr == R.styleable.Switch_sw_thumbRadius)
+ mThumbRadius = a.getDimensionPixelSize(attr, 0);
+ else if (attr == R.styleable.Switch_sw_thumbElevation) {
+ mShadowSize = a.getDimensionPixelSize(attr, 0);
+ mShadowOffset = mShadowSize / 2;
+ } else if (attr == R.styleable.Switch_sw_animDuration)
+ mMaxAnimDuration = a.getInt(attr, 0);
+ else if (attr == R.styleable.Switch_android_gravity)
+ mGravity = a.getInt(attr, 0);
+ else if (attr == R.styleable.Switch_android_checked)
+ setCheckedImmediately(a.getBoolean(attr, mChecked));
+ else if (attr == R.styleable.Switch_sw_interpolator) {
+ int resId = a.getResourceId(R.styleable.Switch_sw_interpolator, 0);
+ if (resId != 0)
+ mInterpolator = AnimationUtils.loadInterpolator(context, resId);
+ }
+ }
+
+ a.recycle();
+
+ if (mTrackSize < 0)
+ mTrackSize = ThemeUtil.dpToPx(context, 2);
+
+ if (mThumbRadius < 0)
+ mThumbRadius = ThemeUtil.dpToPx(context, 8);
+
+ if (mShadowSize < 0) {
+ mShadowSize = ThemeUtil.dpToPx(context, 2);
+ mShadowOffset = mShadowSize / 2;
+ }
+
+ if (mMaxAnimDuration < 0)
+ mMaxAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+
+ if (mInterpolator == null)
+ mInterpolator = new DecelerateInterpolator();
+
+ if (mTrackColors == null) {
+ int[][] states = new int[][]{
+ new int[]{-android.R.attr.state_checked},
+ new int[]{android.R.attr.state_checked},
+ };
+ int[] colors = new int[]{
+ ColorUtil.getColor(ThemeUtil.colorControlNormal(context, 0xFF000000), 0.5f),
+ ColorUtil.getColor(ThemeUtil.colorControlActivated(context, 0xFF000000), 0.5f),
+ };
+
+ mTrackColors = new ColorStateList(states, colors);
+ }
+
+ if (mThumbColors == null) {
+ int[][] states = new int[][]{
+ new int[]{-android.R.attr.state_checked},
+ new int[]{android.R.attr.state_checked},
+ };
+ int[] colors = new int[]{
+ 0xFAFAFA,
+ ThemeUtil.colorControlActivated(context, 0xFF000000),
+ };
+
+ mThumbColors = new ColorStateList(states, colors);
+ }
+
+ mPaint.setStrokeCap(mTrackCap);
+ buildShadow();
+ invalidate();
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if (mCurrentStyle != style) {
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if (mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void setBackground(Drawable drawable) {
+ Drawable background = getBackground();
+ if (background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
+ ((RippleDrawable) background).setBackgroundDrawable(drawable);
+ else
+ super.setBackground(drawable);
+ }
+
+ protected RippleManager getRippleManager() {
+ if (mRippleManager == null) {
+ synchronized (RippleManager.class) {
+ if (mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ /**
+ * Set a listener will be called when the checked state is changed.
+ *
+ * @param listener The {@link OnCheckedChangeListener} will be called.
+ */
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ mOnCheckedChangeListener = listener;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ if (mOnCheckedChangeListener != null)
+ mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
+ }
+
+ float desPos = mChecked ? 1f : 0f;
+
+ if (mThumbPosition != desPos)
+ startAnimation();
+ }
+
+ /**
+ * Change the checked state of this Switch immediately without showing animation.
+ *
+ * @param checked The checked state.
+ */
+ public void setCheckedImmediately(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ if (mOnCheckedChangeListener != null)
+ mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
+ }
+ mThumbPosition = mChecked ? 1f : 0f;
+ invalidate();
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void toggle() {
+ if (isEnabled())
+ setChecked(!mChecked);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
+ if (mIsRtl != rtl) {
+ mIsRtl = rtl;
+ invalidate();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ super.onTouchEvent(event);
+ getRippleManager().onTouchEvent(this, event);
+
+ float x = event.getX();
+ if (mIsRtl)
+ x = 2 * mDrawRect.centerX() - x;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (getParent() != null)
+ getParent().requestDisallowInterceptTouchEvent(true);
+ mMemoX = x;
+ mStartX = mMemoX;
+ mStartTime = SystemClock.uptimeMillis();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float offset = (x - mMemoX) / (mDrawRect.width() - mThumbRadius * 2);
+ mThumbPosition = Math.min(1f, Math.max(0f, mThumbPosition + offset));
+ mMemoX = x;
+ invalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (getParent() != null)
+ getParent().requestDisallowInterceptTouchEvent(false);
+
+ float velocity = (x - mStartX) / (SystemClock.uptimeMillis() - mStartTime) * 1000;
+ if (Math.abs(velocity) >= mFlingVelocity)
+ setChecked(velocity > 0);
+ else if ((!mChecked && mThumbPosition < 0.1f) || (mChecked && mThumbPosition > 0.9f))
+ toggle();
+ else
+ setChecked(mThumbPosition > 0.5f);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (getParent() != null)
+ getParent().requestDisallowInterceptTouchEvent(false);
+
+ setChecked(mThumbPosition > 0.5f);
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ switch (widthMode) {
+ case MeasureSpec.UNSPECIFIED:
+ widthSize = getSuggestedMinimumWidth();
+ break;
+ case MeasureSpec.AT_MOST:
+ widthSize = Math.min(widthSize, getSuggestedMinimumWidth());
+ break;
+ }
+
+ switch (heightMode) {
+ case MeasureSpec.UNSPECIFIED:
+ heightSize = getSuggestedMinimumHeight();
+ break;
+ case MeasureSpec.AT_MOST:
+ heightSize = Math.min(heightSize, getSuggestedMinimumHeight());
+ break;
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ @Override
+ public int getSuggestedMinimumWidth() {
+ return mThumbRadius * 4 + Math.max(mShadowSize, getPaddingLeft()) + Math.max(mShadowSize, getPaddingRight());
+ }
+
+ @Override
+ public int getSuggestedMinimumHeight() {
+ return mThumbRadius * 2 + Math.max(mShadowSize - mShadowOffset, getPaddingTop()) + Math.max(mShadowSize + mShadowOffset, getPaddingBottom());
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mDrawRect.left = Math.max(mShadowSize, getPaddingLeft());
+ mDrawRect.right = w - Math.max(mShadowSize, getPaddingRight());
+
+ int height = mThumbRadius * 2;
+ int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (align) {
+ case Gravity.TOP:
+ mDrawRect.top = Math.max(mShadowSize - mShadowOffset, getPaddingTop());
+ mDrawRect.bottom = mDrawRect.top + height;
+ break;
+ case Gravity.BOTTOM:
+ mDrawRect.bottom = h - Math.max(mShadowSize + mShadowOffset, getPaddingBottom());
+ mDrawRect.top = mDrawRect.bottom - height;
+ break;
+ default:
+ mDrawRect.top = (h - height) / 2f;
+ mDrawRect.bottom = mDrawRect.top + height;
+ break;
+ }
+ }
+
+ private int getTrackColor(boolean checked) {
+ mTempStates[0] = isEnabled() ? android.R.attr.state_enabled : -android.R.attr.state_enabled;
+ mTempStates[1] = checked ? android.R.attr.state_checked : -android.R.attr.state_checked;
+
+ return mTrackColors.getColorForState(mTempStates, 0);
+ }
+
+ private int getThumbColor(boolean checked) {
+ mTempStates[0] = isEnabled() ? android.R.attr.state_enabled : -android.R.attr.state_enabled;
+ mTempStates[1] = checked ? android.R.attr.state_checked : -android.R.attr.state_checked;
+
+ return mThumbColors.getColorForState(mTempStates, 0);
+ }
+
+ private void buildShadow() {
+ if (mShadowSize <= 0)
+ return;
+
+ if (mShadowPaint == null) {
+ mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
+ mShadowPaint.setStyle(Paint.Style.FILL);
+ mShadowPaint.setDither(true);
+ }
+ float startRatio = (float) mThumbRadius / (mThumbRadius + mShadowSize + mShadowOffset);
+ mShadowPaint.setShader(new RadialGradient(0, 0, mThumbRadius + mShadowSize,
+ new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END},
+ new float[]{0f, startRatio, 1f}
+ , Shader.TileMode.CLAMP));
+
+ if (mShadowPath == null) {
+ mShadowPath = new Path();
+ mShadowPath.setFillType(Path.FillType.EVEN_ODD);
+ } else
+ mShadowPath.reset();
+ float radius = mThumbRadius + mShadowSize;
+ mTempRect.set(-radius, -radius, radius, radius);
+ mShadowPath.addOval(mTempRect, Path.Direction.CW);
+ radius = mThumbRadius - 1;
+ mTempRect.set(-radius, -radius - mShadowOffset, radius, radius - mShadowOffset);
+ mShadowPath.addOval(mTempRect, Path.Direction.CW);
+ }
+
+ private void getTrackPath(float x, float y, float radius) {
+ float halfStroke = mTrackSize / 2f;
+
+ mTrackPath.reset();
+
+ if (mTrackCap != Paint.Cap.ROUND) {
+ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
+ float angle = (float) (Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);
+
+ if (x - radius > mDrawRect.left) {
+ mTrackPath.moveTo(mDrawRect.left, y - halfStroke);
+ mTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
+ mTrackPath.lineTo(mDrawRect.left, y + halfStroke);
+ mTrackPath.close();
+ }
+
+ if (x + radius < mDrawRect.right) {
+ mTrackPath.moveTo(mDrawRect.right, y - halfStroke);
+ mTrackPath.arcTo(mTempRect, -angle, angle * 2);
+ mTrackPath.lineTo(mDrawRect.right, y + halfStroke);
+ mTrackPath.close();
+ }
+ } else {
+ float angle = (float) (Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);
+
+ if (x - radius > mDrawRect.left) {
+ float angle2 = (float) (Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180);
+
+ mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
+ mTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2);
+
+ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
+ mTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
+ mTrackPath.close();
+ }
+
+ if (x + radius < mDrawRect.right) {
+ float angle2 = (float) Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke));
+ mTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke));
+
+ angle2 = (float) (angle2 / Math.PI * 180);
+ mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
+ mTrackPath.arcTo(mTempRect, angle2, -angle2 * 2);
+
+ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
+ mTrackPath.arcTo(mTempRect, -angle, angle * 2);
+ mTrackPath.close();
+ }
+ }
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+
+ float x = (mDrawRect.width() - mThumbRadius * 2) * mThumbPosition + mDrawRect.left + mThumbRadius;
+ if (mIsRtl)
+ x = 2 * mDrawRect.centerX() - x;
+ float y = mDrawRect.centerY();
+
+ getTrackPath(x, y, mThumbRadius);
+ mPaint.setColor(ColorUtil.getMiddleColor(getTrackColor(false), getTrackColor(true), mThumbPosition));
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawPath(mTrackPath, mPaint);
+
+ if (mShadowSize > 0) {
+ int saveCount = canvas.save();
+ canvas.translate(x, y + mShadowOffset);
+ canvas.drawPath(mShadowPath, mShadowPaint);
+ canvas.restoreToCount(saveCount);
+ }
+
+ mPaint.setColor(ColorUtil.getMiddleColor(getThumbColor(false), getThumbColor(true), mThumbPosition));
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(x, y, mThumbRadius, mPaint);
+ }
+
+ private void resetAnimation() {
+ mStartTime = SystemClock.uptimeMillis();
+ mStartPosition = mThumbPosition;
+ mAnimDuration = (int) (mMaxAnimDuration * (mChecked ? (1f - mStartPosition) : mStartPosition));
+ }
+
+ private void startAnimation() {
+ if (getHandler() != null) {
+ resetAnimation();
+ mRunning = true;
+ getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ } else
+ mThumbPosition = mChecked ? 1f : 0f;
+ invalidate();
+ }
+
+ private void stopAnimation() {
+ mRunning = false;
+ mThumbPosition = mChecked ? 1f : 0f;
+ if (getHandler() != null)
+ getHandler().removeCallbacks(mUpdater);
+ invalidate();
+ }
+
+ private final Runnable mUpdater = new Runnable() {
+
+ @Override
+ public void run() {
+ update();
+ }
+
+ };
+
+ private void update() {
+ long curTime = SystemClock.uptimeMillis();
+ float progress = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration);
+ float value = mInterpolator.getInterpolation(progress);
+
+ mThumbPosition = mChecked ? (mStartPosition * (1 - value) + value) : (mStartPosition * (1 - value));
+
+ if (progress == 1f)
+ stopAnimation();
+
+ if (mRunning) {
+ if (getHandler() != null)
+ getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ else
+ stopAnimation();
+ }
+
+ invalidate();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ ss.checked = isChecked();
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+ setChecked(ss.checked);
+ requestLayout();
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean checked;
+
+ /**
+ * Constructor called from {@link Switch#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ checked = (Boolean) in.readValue(null);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeValue(checked);
+ }
+
+ @Override
+ public String toString() {
+ return "Switch.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " checked=" + checked + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/TabIndicatorView.java b/material/src/main/java/com/rey/material/widget/TabIndicatorView.java
new file mode 100644
index 0000000..9e12473
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/TabIndicatorView.java
@@ -0,0 +1,768 @@
+package com.rey.material.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.view.ViewPager;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Checkable;
+import android.widget.ImageView;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.ViewUtil;
+
+/**
+ * Created by Rey on 9/15/2015.
+ */
+public class TabIndicatorView extends RecyclerView implements ThemeManager.OnThemeChangedListener{
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ private int mMode;
+ private int mTabPadding;
+ private int mTabRippleStyle;
+ private int mTextAppearance;
+ private boolean mTabSingleLine;
+ private boolean mCenterCurrentTab;
+
+ private int mIndicatorOffset;
+ private int mIndicatorWidth;
+ private int mIndicatorHeight;
+ private boolean mIndicatorAtTop;
+
+ private Paint mPaint;
+
+ public static final int MODE_SCROLL = 0;
+ public static final int MODE_FIXED = 1;
+
+ public static final int SCROLL_STATE_IDLE = 0;
+ public static final int SCROLL_STATE_DRAGGING = 1;
+ public static final int SCROLL_STATE_SETTLING = 2;
+
+ private int mSelectedPosition;
+ private boolean mScrolling;
+ private boolean mIsRtl;
+
+ private LayoutManager mLayoutManager;
+ private Adapter mAdapter;
+ private TabIndicatorFactory mFactory;
+
+ private Runnable mTabAnimSelector;
+
+ private boolean mScrollingToCenter = false;
+
+ public TabIndicatorView(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public TabIndicatorView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public TabIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ setHorizontalScrollBarEnabled(false);
+
+ mTabPadding = -1;
+ mTabSingleLine = true;
+ mCenterCurrentTab = false;
+ mIndicatorHeight = -1;
+ mIndicatorAtTop = false;
+ mScrolling = false;
+ mIsRtl = false;
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(ThemeUtil.colorAccent(context, 0xFFFFFFFF));
+
+ mAdapter = new Adapter();
+ setAdapter(mAdapter);
+ mLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, mIsRtl);
+ setLayoutManager(mLayoutManager);
+ setItemAnimator(new DefaultItemAnimator());
+ addOnScrollListener(new OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ updateIndicator(mLayoutManager.findViewByPosition(mSelectedPosition));
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ updateIndicator(mLayoutManager.findViewByPosition(mSelectedPosition));
+ }
+
+ });
+
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPageIndicator, defStyleAttr, defStyleRes);
+
+ int tabPadding = -1;
+ int textAppearance = 0;
+ int mode = -1;
+ int rippleStyle = 0;
+ boolean tabSingleLine = false;
+ boolean singleLineDefined = false;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+ if(attr == R.styleable.TabPageIndicator_tpi_tabPadding)
+ tabPadding = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_tabRipple)
+ rippleStyle = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_indicatorColor)
+ mPaint.setColor(a.getColor(attr, 0));
+ else if(attr == R.styleable.TabPageIndicator_tpi_indicatorHeight)
+ mIndicatorHeight = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_indicatorAtTop)
+ mIndicatorAtTop = a.getBoolean(attr, true);
+ else if(attr == R.styleable.TabPageIndicator_tpi_tabSingleLine) {
+ tabSingleLine = a.getBoolean(attr, true);
+ singleLineDefined = true;
+ }
+ else if(attr == R.styleable.TabPageIndicator_tpi_centerCurrentTab)
+ mCenterCurrentTab = a.getBoolean(attr, true);
+ else if(attr == R.styleable.TabPageIndicator_android_textAppearance)
+ textAppearance = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_mode)
+ mode = a.getInteger(attr, 0);
+ }
+
+ a.recycle();
+
+ if(mIndicatorHeight < 0)
+ mIndicatorHeight = ThemeUtil.dpToPx(context, 2);
+
+ boolean shouldNotify = false;
+
+ if(tabPadding >= 0 && mTabPadding != tabPadding){
+ mTabPadding = tabPadding;
+ shouldNotify = true;
+ }
+
+ if(singleLineDefined && mTabSingleLine != tabSingleLine){
+ mTabSingleLine = tabSingleLine;
+ shouldNotify = true;
+ }
+
+ if(mode >= 0 && mMode != mode){
+ mMode = mode;
+ mAdapter.setFixedWidth(0, 0);
+ shouldNotify = true;
+ }
+
+ if(textAppearance != 0 && mTextAppearance != textAppearance){
+ mTextAppearance = textAppearance;
+ shouldNotify = true;
+ }
+
+ if(rippleStyle != 0 && rippleStyle != mTabRippleStyle){
+ mTabRippleStyle = rippleStyle;
+ shouldNotify = true;
+ }
+
+ if(shouldNotify)
+ mAdapter.notifyItemRangeChanged(0, mAdapter.getItemCount());
+
+ invalidate();
+ }
+
+ public void setTabIndicatorFactory(TabIndicatorFactory factory){
+ mFactory = factory;
+ mAdapter.setFactory(factory);
+ }
+
+ private void animateToTab(final int position) {
+ if(position < 0 || position >= mAdapter.getItemCount())
+ return;
+
+ if (mTabAnimSelector != null)
+ removeCallbacks(mTabAnimSelector);
+
+ mTabAnimSelector = new Runnable() {
+ public void run() {
+ View v = mLayoutManager.findViewByPosition(position);
+ if(!mScrolling)
+ updateIndicator(v);
+
+ smoothScrollToPosition(mSelectedPosition);
+ mTabAnimSelector = null;
+ }
+ };
+
+ post(mTabAnimSelector);
+ }
+
+ private void updateIndicator(int offset, int width){
+ mIndicatorOffset = offset;
+ mIndicatorWidth = width;
+ invalidate();
+ }
+
+ private void updateIndicator(View anchorView){
+ if(anchorView != null) {
+ updateIndicator(anchorView.getLeft(), anchorView.getMeasuredWidth());
+ ((Checkable)anchorView).setChecked(true);
+ }
+ else {
+ updateIndicator(getWidth(), 0);
+ }
+ }
+
+ /**
+ * Set the current tab of this TabIndicatorView.
+ * @param position The position of current tab.
+ */
+ public void setCurrentTab(int position) {
+ if(mSelectedPosition != position){
+ View v = mLayoutManager.findViewByPosition(mSelectedPosition);
+ if(v != null)
+ ((Checkable)v).setChecked(false);
+ }
+
+ mSelectedPosition = position;
+ View v = mLayoutManager.findViewByPosition(mSelectedPosition);
+ if(v != null)
+ ((Checkable)v).setChecked(true);
+
+ animateToTab(position);
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ // Re-post the selector we saved
+ if (mTabAnimSelector != null)
+ post(mTabAnimSelector);
+
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mTabAnimSelector != null)
+ removeCallbacks(mTabAnimSelector);
+
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
+ if(mIsRtl != rtl) {
+ mIsRtl = rtl;
+ mLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, mIsRtl);
+ setLayoutManager(mLayoutManager);
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ super.onMeasure(widthSpec, heightSpec);
+
+ if(mMode == MODE_FIXED){
+ int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ int count = mAdapter.getItemCount();
+ if(count > 0) {
+ int width = totalWidth / count;
+ int lastWidth = totalWidth - width * (count - 1);
+ mAdapter.setFixedWidth(width, lastWidth);
+ }
+ else
+ mAdapter.setFixedWidth(totalWidth, totalWidth);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateIndicator(mLayoutManager.findViewByPosition(mSelectedPosition));
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+
+ int x = mIndicatorOffset;
+ int y = mIndicatorAtTop ? 0 : getHeight() - mIndicatorHeight;
+ canvas.drawRect(x, y, x + mIndicatorWidth, y + mIndicatorHeight, mPaint);
+
+ //TODO: handle it
+
+// if(isInEditMode())
+// canvas.drawRect(getPaddingLeft(), y, getPaddingLeft() + mTabContainer.getChildAt(0).getWidth(), y + mIndicatorHeight, mPaint);
+ }
+
+ protected void onTabScrollStateChanged(int state){
+ if(mCenterCurrentTab) {
+ if (state == SCROLL_STATE_IDLE) {
+ if (!mScrollingToCenter) {
+ View v = mLayoutManager.findViewByPosition(mSelectedPosition);
+ if (v != null) {
+ int viewCenter = (v.getLeft() + v.getRight()) / 2;
+ int parentCenter = (getLeft() + getPaddingLeft() + getRight() - getPaddingRight()) / 2;
+ int scrollNeeded = viewCenter - parentCenter;
+ if (scrollNeeded != 0) {
+ smoothScrollBy(scrollNeeded, 0);
+ mScrollingToCenter = true;
+ }
+ }
+ }
+ }
+
+ if (state == SCROLL_STATE_DRAGGING || state == SCROLL_STATE_SETTLING)
+ mScrollingToCenter = false;
+ }
+
+ if(state == ViewPager.SCROLL_STATE_IDLE){
+ mScrolling = false;
+ View v = mLayoutManager.findViewByPosition(mSelectedPosition);
+ updateIndicator(v);
+ }
+ else
+ mScrolling = true;
+ }
+
+ protected void onTabScrolled(int position, float positionOffset) {
+ View scrollView = mLayoutManager.findViewByPosition(position);
+ View nextView = mLayoutManager.findViewByPosition(position + 1);
+
+ if(scrollView != null && nextView != null){
+ int width_scroll = scrollView.getMeasuredWidth();
+ int width_next = nextView.getMeasuredWidth();
+ float distance = (width_scroll + width_next) / 2f;
+
+ int width = (int)(width_scroll + (width_next - width_scroll) * positionOffset + 0.5f);
+ int offset = (int)(scrollView.getLeft() + width_scroll / 2f + distance * positionOffset - width / 2f + 0.5f);
+ updateIndicator(offset, width);
+ }
+ }
+
+ protected void onTabSelected(int position){
+ setCurrentTab(position);
+ }
+
+ public static abstract class TabIndicatorFactory {
+
+ private TabIndicatorView mView;
+
+ /**
+ * Get the number of tab indicators.
+ * @return
+ */
+ public abstract int getTabIndicatorCount();
+
+ /**
+ * Check if the tab indicator at specific position is icon or text.
+ * @param position The position of tab indicator.
+ * @return
+ */
+ public abstract boolean isIconTabIndicator(int position);
+
+ /**
+ * Get the icon for tab indicator at specific position.
+ * @param position The position of tab indicator.
+ * @return
+ */
+ public abstract Drawable getIcon(int position);
+
+ /**
+ * Get the text for tab indicator at specific position.
+ * @param position The position of tab indicator.
+ * @return
+ */
+ public abstract CharSequence getText(int position);
+
+ /**
+ * Get the current selected tab.
+ * @return
+ */
+ public abstract int getCurrentTabIndicator();
+
+ /**
+ * Notify the selected tab indicator has changed. Your layout should be updated to reflect the changes of TabIndicatorView.
+ * @param position The position of selected tab indicator.
+ */
+ public abstract void onTabIndicatorSelected(int position);
+
+ protected void setTabIndicatorView(TabIndicatorView view){
+ mView = view;
+ }
+
+ /**
+ * Notify the scroll state of your tab layout has changed, and the TabIndicatorView should update to reflect the changes.
+ * @param state The new scroll state.
+ * @see TabIndicatorView#SCROLL_STATE_IDLE
+ * @see TabIndicatorView#SCROLL_STATE_DRAGGING
+ * @see TabIndicatorView#SCROLL_STATE_SETTLING
+ */
+ public final void notifyTabScrollStateChanged(int state){
+ mView.onTabScrollStateChanged(state);
+ }
+
+ /**
+ * Notify the current tab is scrolled, and the TabIndicatorView should update to reflect the changes.
+ *
+ * @param position Position of the first left tab .
+ * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
+ */
+ public final void notifyTabScrolled(int position, float positionOffset) {
+ mView.onTabScrolled(position, positionOffset);
+ }
+
+ /**
+ * Notify a new tab becomes selected, and the TabIndicatorView should update to reflect the changes.
+ * Animation is not necessarily complete.
+ *
+ * @param position Position of the new selected tab.
+ */
+ public final void notifyTabSelected(int position){
+ mView.onTabSelected(position);
+ }
+
+ /**
+ * Notify tab's data set has changed, and the TabIndicatorView should update to reflect the changes.
+ */
+ public final void notifyDataSetChanged(){
+ mView.getAdapter().notifyDataSetChanged();
+ }
+
+ /**
+ * Notify the tab at specific position has beenchanged, and the TabIndicatorView should update to reflect the changes.
+ * @param position Position of the tab.
+ */
+ public final void notifyTabChanged(int position) {
+ mView.getAdapter().notifyItemRangeChanged(position, 1);
+ }
+
+ /**
+ * Notify the range of tab has been changed, and the TabIndicatorView should update to reflect the changes.
+ * @param positionStart The start position of range.
+ * @param itemCount The number of tabs.
+ */
+ public final void notifyTabRangeChanged(int positionStart, int itemCount) {
+ mView.getAdapter().notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ /**
+ * Notify the tab at specific position has been inserted, and the TabIndicatorView should update to reflect the changes.
+ * @param position Position of the tab.
+ */
+ public final void notifyTabInserted(int position) {
+ mView.getAdapter().notifyItemRangeInserted(position, 1);
+ }
+
+ /**
+ * Notify the tab at specific position has been moved, and the TabIndicatorView should update to reflect the changes.
+ * @param fromPosition The old position of the tab.
+ * @param toPosition The new position of the tab.
+ */
+ public final void notifyTabMoved(int fromPosition, int toPosition) {
+ mView.getAdapter().notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /**
+ * Notify the range of tab has been inserted, and the TabIndicatorView should update to reflect the changes.
+ * @param positionStart The start position of range.
+ * @param itemCount The number of tabs.
+ */
+ public final void notifyTabRangeInserted(int positionStart, int itemCount) {
+ mView.getAdapter().notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ /**
+ * Notify the tab at specific position has been removed, and the TabIndicatorView should update to reflect the changes.
+ * @param position Position of the tab.
+ */
+ public final void notifyTabRemoved(int position) {
+ mView.getAdapter().notifyItemRangeRemoved(position, 1);
+ }
+
+ /**
+ * Notify the range of tab has been removed, and the TabIndicatorView should update to reflect the changes.
+ * @param positionStart The start position of range.
+ * @param itemCount The number of tabs.
+ */
+ public final void notifyTabRangeRemoved(int positionStart, int itemCount) {
+ mView.getAdapter().notifyItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ class Adapter extends RecyclerView.Adapter implements View.OnClickListener {
+
+ TabIndicatorFactory mFactory;
+
+ static final int TYPE_TEXT = 0;
+ static final int TYPE_ICON = 1;
+
+ int mFixedWidth;
+ int mLastFixedWidth;
+
+ public void setFactory(TabIndicatorFactory factory){
+ if(mFactory != null)
+ mFactory.setTabIndicatorView(null);
+
+ int prevCount = getItemCount();
+ if(prevCount > 0)
+ notifyItemRangeRemoved(0, prevCount);
+
+ mFactory = factory;
+ if(mFactory != null)
+ mFactory.setTabIndicatorView(TabIndicatorView.this);
+ int count = getItemCount();
+ if(count > 0)
+ notifyItemRangeInserted(0, count);
+
+ if(mFactory != null)
+ onTabSelected(mFactory.getCurrentTabIndicator());
+ }
+
+ public void setFixedWidth(int width, int lastWidth){
+ if(mFixedWidth != width || mLastFixedWidth != lastWidth){
+ mFixedWidth = width;
+ mLastFixedWidth = lastWidth;
+
+ int count = getItemCount();
+ if(count > 0)
+ notifyItemRangeChanged(0, count);
+ }
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = null;
+ switch (viewType){
+ case TYPE_TEXT:
+ v = new CheckedTextView(parent.getContext());
+ break;
+ case TYPE_ICON:
+ v = new ImageButton(parent.getContext());
+ break;
+ }
+
+ ViewHolder holder = new ViewHolder(v);
+ v.setTag(holder);
+ v.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ v.setOnClickListener(this);
+
+ switch (viewType){
+ case TYPE_TEXT:
+ holder.textView.setCheckMarkDrawable(null);
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ holder.textView.setTextAlignment(TEXT_ALIGNMENT_GRAVITY);
+ holder.textView.setGravity(Gravity.CENTER);
+ holder.textView.setEllipsize(TextUtils.TruncateAt.END);
+ holder.textView.setSingleLine(true);
+ break;
+ case TYPE_ICON:
+ holder.iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ break;
+ }
+
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ int viewType = getItemViewType(position);
+
+ ViewGroup.LayoutParams params = holder.itemView.getLayoutParams();
+ if(mFixedWidth > 0)
+ params.width = position == getItemCount() - 1 ? mLastFixedWidth : mFixedWidth;
+ else
+ params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ holder.itemView.setLayoutParams(params);
+
+ if(holder.padding != mTabPadding){
+ holder.padding = mTabPadding;
+ holder.itemView.setPadding(mTabPadding, 0, mTabPadding, 0);
+ }
+
+ if(holder.rippleStyle != mTabRippleStyle){
+ holder.rippleStyle = mTabRippleStyle;
+ if(mTabRippleStyle > 0)
+ ViewUtil.setBackground(holder.itemView, new RippleDrawable.Builder(getContext(), mTabRippleStyle).build());
+ }
+
+ switch (viewType){
+ case TYPE_TEXT:
+ if(holder.textAppearance != mTextAppearance) {
+ holder.textAppearance = mTextAppearance;
+ holder.textView.setTextAppearance(getContext(), mTextAppearance);
+ }
+ if(holder.singleLine != mTabSingleLine) {
+ holder.singleLine = mTabSingleLine;
+ if (mTabSingleLine)
+ holder.textView.setSingleLine(true);
+ else {
+ holder.textView.setSingleLine(false);
+ holder.textView.setMaxLines(2);
+ }
+ }
+
+ holder.textView.setText(mFactory.getText(position));
+ holder.textView.setChecked(position == mSelectedPosition);
+ break;
+ case TYPE_ICON:
+ holder.iconView.setImageDrawable(mFactory.getIcon(position));
+ holder.iconView.setChecked(position == mSelectedPosition);
+ break;
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mFactory.isIconTabIndicator(position) ? TYPE_ICON : TYPE_TEXT;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mFactory == null ? 0 : mFactory.getTabIndicatorCount();
+ }
+
+ @Override
+ public void onClick(View view) {
+ ViewHolder holder = (ViewHolder) view.getTag();
+ mFactory.onTabIndicatorSelected(holder.getAdapterPosition());
+ }
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder{
+
+ CheckedTextView textView;
+
+ CheckedImageView iconView;
+
+ int rippleStyle = 0;
+ boolean singleLine = true;
+ int textAppearance = 0;
+ int padding = 0;
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+ if(itemView instanceof CheckedImageView)
+ iconView = (CheckedImageView)itemView;
+ else if(itemView instanceof CheckedTextView)
+ textView = (CheckedTextView)itemView;
+ }
+
+ }
+
+ public static class ViewPagerIndicatorFactory extends TabIndicatorFactory implements ViewPager.OnPageChangeListener {
+
+ ViewPager mViewPager;
+
+ public ViewPagerIndicatorFactory(ViewPager vp){
+ mViewPager = vp;
+ mViewPager.addOnPageChangeListener(this);
+ }
+
+ @Override
+ public int getTabIndicatorCount() {
+ return mViewPager.getAdapter().getCount();
+ }
+
+ @Override
+ public boolean isIconTabIndicator(int position) {
+ return false;
+ }
+
+ @Override
+ public Drawable getIcon(int position) {
+ return null;
+ }
+
+ @Override
+ public CharSequence getText(int position) {
+ return mViewPager.getAdapter().getPageTitle(position);
+ }
+
+ @Override
+ public void onTabIndicatorSelected(int position) {
+ mViewPager.setCurrentItem(position, true);
+ }
+
+ @Override
+ public int getCurrentTabIndicator() {
+ return mViewPager.getCurrentItem();
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ notifyTabScrolled(position, positionOffset);
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ notifyTabSelected(position);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ switch (state){
+ case ViewPager.SCROLL_STATE_IDLE:
+ notifyTabScrollStateChanged(SCROLL_STATE_IDLE);
+ break;
+ case ViewPager.SCROLL_STATE_DRAGGING:
+ notifyTabScrollStateChanged(SCROLL_STATE_DRAGGING);
+ break;
+ case ViewPager.SCROLL_STATE_SETTLING:
+ notifyTabScrollStateChanged(SCROLL_STATE_SETTLING);
+ break;
+ }
+ }
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/TabPageIndicator.java b/material/src/main/java/com/rey/material/widget/TabPageIndicator.java
new file mode 100644
index 0000000..c9d0b55
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/TabPageIndicator.java
@@ -0,0 +1,646 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.ViewUtil;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class TabPageIndicator extends HorizontalScrollView implements ViewPager.OnPageChangeListener, View.OnClickListener, ThemeManager.OnThemeChangedListener{
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ private TabContainerLayout mTabContainer;
+ private ViewPager mViewPager;
+
+ private int mMode;
+ private int mTabPadding;
+ private int mTabRippleStyle;
+ private int mTextAppearance;
+ private boolean mTabSingleLine;
+
+ private int mIndicatorOffset;
+ private int mIndicatorWidth;
+ private int mIndicatorHeight;
+ private boolean mIndicatorAtTop;
+
+ private Paint mPaint;
+
+ public static final int MODE_SCROLL = 0;
+ public static final int MODE_FIXED = 1;
+
+ private int mSelectedPosition;
+ private boolean mScrolling;
+ private boolean mIsRtl;
+
+ private Runnable mTabAnimSelector;
+
+ private ViewPager.OnPageChangeListener mListener;
+
+ private DataSetObserver mObserver = new DataSetObserver(){
+
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+
+ };
+
+ public TabPageIndicator(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public TabPageIndicator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public TabPageIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public TabPageIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ setHorizontalScrollBarEnabled(false);
+
+ mTabPadding = -1;
+ mTabSingleLine = true;
+ mIndicatorHeight = -1;
+ mIndicatorAtTop = false;
+ mScrolling = false;
+ mIsRtl = false;
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(ThemeUtil.colorAccent(context, 0xFFFFFFFF));
+
+ mTabContainer = new TabContainerLayout(context);
+
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ if(isInEditMode())
+ addTemporaryTab();
+
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPageIndicator, defStyleAttr, defStyleRes);
+
+ int textAppearance = 0;
+ int mode = -1;
+ int rippleStyle = 0;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+ if(attr == R.styleable.TabPageIndicator_tpi_tabPadding)
+ mTabPadding = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_tabRipple)
+ rippleStyle = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_indicatorColor)
+ mPaint.setColor(a.getColor(attr, 0));
+ else if(attr == R.styleable.TabPageIndicator_tpi_indicatorHeight)
+ mIndicatorHeight = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_indicatorAtTop)
+ mIndicatorAtTop = a.getBoolean(attr, true);
+ else if(attr == R.styleable.TabPageIndicator_tpi_tabSingleLine)
+ mTabSingleLine = a.getBoolean(attr, true);
+ else if(attr == R.styleable.TabPageIndicator_android_textAppearance)
+ textAppearance = a.getResourceId(attr, 0);
+ else if(attr == R.styleable.TabPageIndicator_tpi_mode)
+ mode = a.getInteger(attr, 0);
+ }
+
+ a.recycle();
+
+ if(mTabPadding < 0)
+ mTabPadding = ThemeUtil.dpToPx(context, 12);
+
+ if(mIndicatorHeight < 0)
+ mIndicatorHeight = ThemeUtil.dpToPx(context, 2);
+
+ if(mode >= 0){
+ if(mMode != mode || getChildCount() == 0){
+ mMode = mode;
+ removeAllViews();
+ if(mMode == MODE_SCROLL) {
+ addView(mTabContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ setFillViewport(false);
+ }
+ else if(mMode == MODE_FIXED){
+ addView(mTabContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ setFillViewport(true);
+ }
+ }
+ }
+
+ if(textAppearance != 0 && mTextAppearance != textAppearance){
+ mTextAppearance = textAppearance;
+ for(int i = 0, count = mTabContainer.getChildCount(); i < count; i++){
+ CheckedTextView tv = (CheckedTextView)mTabContainer.getChildAt(i);
+ tv.setTextAppearance(context, mTextAppearance);
+ }
+ }
+
+ if(rippleStyle != 0 && rippleStyle != mTabRippleStyle){
+ mTabRippleStyle = rippleStyle;
+ for(int i = 0, count = mTabContainer.getChildCount(); i < count; i++)
+ ViewUtil.setBackground(mTabContainer.getChildAt(i), new RippleDrawable.Builder(getContext(), mTabRippleStyle).build());
+ }
+
+ if(mViewPager != null)
+ notifyDataSetChanged();
+ requestLayout();
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ // Re-post the selector we saved
+ if (mTabAnimSelector != null)
+ post(mTabAnimSelector);
+
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mTabAnimSelector != null)
+ removeCallbacks(mTabAnimSelector);
+
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
+ if(mIsRtl != rtl) {
+ mIsRtl = rtl;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int ws = widthMeasureSpec;
+ if(ws != MeasureSpec.UNSPECIFIED)
+ ws = MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight(), widthMode);
+
+ int hs = heightMeasureSpec;
+ if(heightMode != MeasureSpec.UNSPECIFIED)
+ hs = MeasureSpec.makeMeasureSpec(heightSize - getPaddingTop() - getPaddingBottom(), heightMode);
+
+ mTabContainer.measure(ws, hs);
+
+ int width = 0;
+ switch (widthMode){
+ case MeasureSpec.UNSPECIFIED:
+ width = mTabContainer.getMeasuredWidth() + getPaddingLeft() + getPaddingRight();
+ break;
+ case MeasureSpec.AT_MOST:
+ width = Math.min(mTabContainer.getMeasuredWidth() + getPaddingLeft() + getPaddingRight(), widthSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ width = widthSize;
+ break;
+ }
+
+ int height = 0;
+ switch (heightMode){
+ case MeasureSpec.UNSPECIFIED:
+ height = mTabContainer.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
+ break;
+ case MeasureSpec.AT_MOST:
+ height = Math.min(mTabContainer.getMeasuredHeight() + getPaddingTop() + getPaddingBottom(), heightSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ height = heightSize;
+ break;
+ }
+
+ if(mTabContainer.getMeasuredWidth() != width - getPaddingLeft() - getPaddingRight() || mTabContainer.getMeasuredHeight() != height - getPaddingTop() - getPaddingBottom())
+ mTabContainer.measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
+
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ TextView tv = getTabView(mSelectedPosition);
+ if(tv != null)
+ updateIndicator(tv.getLeft(), tv.getMeasuredWidth());
+ }
+
+ private CheckedTextView getTabView(int position){
+ return (CheckedTextView)mTabContainer.getChildAt(position);
+ }
+
+ private void animateToTab(final int position) {
+ if(getTabView(position) == null)
+ return;
+
+ if (mTabAnimSelector != null)
+ removeCallbacks(mTabAnimSelector);
+
+ mTabAnimSelector = new Runnable() {
+ public void run() {
+ CheckedTextView tv = getTabView(position);
+ if(tv != null) {
+ if (!mScrolling)
+ updateIndicator(tv.getLeft(), tv.getMeasuredWidth());
+
+ smoothScrollTo(tv.getLeft() - (getWidth() - tv.getWidth()) / 2 + getPaddingLeft(), 0);
+ }
+ mTabAnimSelector = null;
+ }
+ };
+
+ post(mTabAnimSelector);
+ }
+
+ /**
+ * Set a listener will be called when the current page is changed.
+ * @param listener The {@link ViewPager.OnPageChangeListener} will be called.
+ */
+ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Set the ViewPager associate with this indicator view.
+ * @param view The ViewPager view.
+ */
+ public void setViewPager(@Nullable ViewPager view) {
+ if (mViewPager == view)
+ return;
+
+ if (mViewPager != null){
+ mViewPager.removeOnPageChangeListener(this);
+ PagerAdapter adapter = mViewPager.getAdapter();
+ if(adapter != null)
+ adapter.unregisterDataSetObserver(mObserver);
+ }
+
+ mViewPager = view;
+
+ if(mViewPager != null) {
+ PagerAdapter adapter = mViewPager.getAdapter();
+ if (adapter == null)
+ throw new IllegalStateException("ViewPager does not have adapter instance.");
+
+ adapter.registerDataSetObserver(mObserver);
+
+ mViewPager.addOnPageChangeListener(this);
+
+ notifyDataSetChanged();
+ onPageSelected(mViewPager.getCurrentItem());
+ }
+ else
+ mTabContainer.removeAllViews();
+ }
+
+ /**
+ * Set the ViewPager associate with this indicator view and the current position;
+ * @param view The ViewPager view.
+ * @param initialPosition The current position.
+ */
+ public void setViewPager(ViewPager view, int initialPosition) {
+ setViewPager(view);
+ setCurrentItem(initialPosition);
+ }
+
+ private void updateIndicator(int offset, int width){
+ mIndicatorOffset = offset;
+ mIndicatorWidth = width;
+ invalidate();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ super.draw(canvas);
+
+ int x = mIndicatorOffset + getPaddingLeft();
+ int y = mIndicatorAtTop ? 0 : getHeight() - mIndicatorHeight;
+ canvas.drawRect(x, y, x + mIndicatorWidth, y + mIndicatorHeight, mPaint);
+
+ if(isInEditMode())
+ canvas.drawRect(getPaddingLeft(), y, getPaddingLeft() + mTabContainer.getChildAt(0).getWidth(), y + mIndicatorHeight, mPaint);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if(state == ViewPager.SCROLL_STATE_IDLE){
+ mScrolling = false;
+ TextView tv = getTabView(mSelectedPosition);
+ if(tv != null) {
+ updateIndicator(tv.getLeft(), tv.getMeasuredWidth());
+ }
+ }
+ else
+ mScrolling = true;
+
+ if (mListener != null)
+ mListener.onPageScrollStateChanged(state);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ if (mListener != null)
+ mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
+
+ CheckedTextView tv_scroll = getTabView(position);
+ CheckedTextView tv_next = getTabView(position + 1);
+
+ if(tv_scroll != null && tv_next != null){
+ int width_scroll = tv_scroll.getMeasuredWidth();
+ int width_next = tv_next.getMeasuredWidth();
+ float distance = (width_scroll + width_next) / 2f;
+
+ int width = (int)(width_scroll + (width_next - width_scroll) * positionOffset + 0.5f);
+ int offset = (int)(tv_scroll.getLeft() + width_scroll / 2f + distance * positionOffset - width / 2f + 0.5f);
+ updateIndicator(offset, width);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ setCurrentItem(position);
+ if (mListener != null)
+ mListener.onPageSelected(position);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int position = (Integer)v.getTag();
+ if(position == mSelectedPosition && mListener != null)
+ mListener.onPageSelected(position);
+
+ mViewPager.setCurrentItem(position, true);
+ }
+
+ /**
+ * Set the current page of this TabPageIndicator.
+ * @param position The position of current page.
+ */
+ public void setCurrentItem(int position) {
+ if(mSelectedPosition != position){
+ CheckedTextView tv = getTabView(mSelectedPosition);
+ if(tv != null)
+ tv.setChecked(false);
+ }
+
+ mSelectedPosition = position;
+ CheckedTextView tv = getTabView(mSelectedPosition);
+ if(tv != null)
+ tv.setChecked(true);
+
+ animateToTab(position);
+ }
+
+ private void notifyDataSetChanged() {
+ mTabContainer.removeAllViews();
+
+ PagerAdapter adapter = mViewPager.getAdapter();
+ final int count = adapter.getCount();
+
+ if (mSelectedPosition > count)
+ mSelectedPosition = count - 1;
+
+ for (int i = 0; i < count; i++) {
+ CharSequence title = adapter.getPageTitle(i);
+ if (title == null)
+ title = "NULL";
+
+ CheckedTextView tv = new CheckedTextView(getContext());
+ tv.setCheckMarkDrawable(null);
+ tv.setText(title);
+ tv.setGravity(Gravity.CENTER);
+ tv.setTextAppearance(getContext(), mTextAppearance);
+ if(mTabSingleLine)
+ tv.setSingleLine(true);
+ else {
+ tv.setSingleLine(false);
+ tv.setMaxLines(2);
+ }
+ tv.setEllipsize(TruncateAt.END);
+ tv.setOnClickListener(this);
+ tv.setTag(i);
+ if(mTabRippleStyle > 0)
+ ViewUtil.setBackground(tv, new RippleDrawable.Builder(getContext(), mTabRippleStyle).build());
+
+ tv.setPadding(mTabPadding, 0, mTabPadding, 0);
+ mTabContainer.addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ }
+
+ setCurrentItem(mSelectedPosition);
+ requestLayout();
+ }
+
+ private void notifyDataSetInvalidated() {
+ PagerAdapter adapter = mViewPager.getAdapter();
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ TextView tv = getTabView(i);
+
+ if(tv != null) {
+ CharSequence title = adapter.getPageTitle(i);
+ if (title == null)
+ title = "NULL";
+
+ tv.setText(title);
+ }
+ }
+
+ requestLayout();
+ }
+
+ private void addTemporaryTab(){
+ for (int i = 0; i < 3; i++) {
+ CharSequence title = null;
+ if (i == 0)
+ title = "TAB ONE";
+ else if (i == 1)
+ title = "TAB TWO";
+ else if (i == 2)
+ title = "TAB THREE";
+
+ CheckedTextView tv = new CheckedTextView(getContext());
+ tv.setCheckMarkDrawable(null);
+ tv.setText(title);
+ tv.setGravity(Gravity.CENTER);
+ tv.setTextAppearance(getContext(), mTextAppearance);
+ tv.setSingleLine(true);
+ tv.setEllipsize(TruncateAt.END);
+ tv.setTag(i);
+ tv.setChecked(i == 0);
+ if(mMode == MODE_SCROLL){
+ tv.setPadding(mTabPadding, 0, mTabPadding, 0);
+ mTabContainer.addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ }
+ else if(mMode == MODE_FIXED){
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT);
+ params.weight = 1f;
+ mTabContainer.addView(tv, params);
+ }
+ }
+ }
+
+ private class TabContainerLayout extends FrameLayout{
+
+ public TabContainerLayout(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int width = 0;
+ int height = 0;
+
+ if(mMode == MODE_SCROLL){
+ int ws = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ child.measure(ws, heightMeasureSpec);
+ width += child.getMeasuredWidth();
+ height = Math.max(height, child.getMeasuredHeight());
+ }
+ setMeasuredDimension(width, height);
+ }
+ else{
+ if(widthMode != MeasureSpec.EXACTLY){
+ int ws = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ child.measure(ws, heightMeasureSpec);
+ width += child.getMeasuredWidth();
+ height = Math.max(height, child.getMeasuredHeight());
+ }
+
+ if(widthMode == MeasureSpec.UNSPECIFIED || width < widthSize)
+ setMeasuredDimension(widthSize, height);
+ else{
+ int count = getChildCount();
+ int childWidth = count == 0 ? 0 : widthSize / count;
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if(i != count - 1)
+ child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
+ else
+ child.measure(MeasureSpec.makeMeasureSpec(widthSize - childWidth * (count - 1), MeasureSpec.EXACTLY), heightMeasureSpec);
+ }
+ setMeasuredDimension(widthSize, height);
+ }
+ }
+ else {
+ int count = getChildCount();
+ int childWidth = count == 0 ? 0 : widthSize / count;
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if(i != count - 1)
+ child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), heightMeasureSpec);
+ else
+ child.measure(MeasureSpec.makeMeasureSpec(widthSize - childWidth * (count - 1), MeasureSpec.EXACTLY), heightMeasureSpec);
+ height = Math.max(height, child.getMeasuredHeight());
+ }
+ setMeasuredDimension(widthSize, height);
+ }
+ }
+
+ int hs = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if(child.getMeasuredHeight() != height)
+ child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY), hs);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int childLeft = 0;
+ int childTop = 0;
+ int childRight = right - left;
+ int childBottom = bottom - top;
+
+ if(mIsRtl)
+ for(int i = 0, count = getChildCount(); i < count; i++){
+ View child = getChildAt(i);
+ child.layout(childRight - child.getMeasuredWidth(), childTop, childRight, childBottom);
+ childRight -= child.getMeasuredWidth();
+ }
+ else
+ for(int i = 0, count = getChildCount(); i < count; i++){
+ View child = getChildAt(i);
+ child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childBottom);
+ childLeft += child.getMeasuredWidth();
+ }
+ }
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/TextView.java b/material/src/main/java/com/rey/material/widget/TextView.java
new file mode 100644
index 0000000..58e31a8
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/TextView.java
@@ -0,0 +1,150 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.rey.material.util.ThemeManager;
+import com.rey.material.drawable.RippleDrawable;
+import com.rey.material.util.RippleManager;
+import com.rey.material.util.ViewUtil;
+
+public class TextView extends android.widget.TextView implements ThemeManager.OnThemeChangedListener{
+
+ private RippleManager mRippleManager;
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ public interface OnSelectionChangedListener{
+ void onSelectionChanged(View v, int selStart, int selEnd);
+ }
+
+ private OnSelectionChangedListener mOnSelectionChangedListener;
+
+ public TextView(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public TextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ ViewUtil.applyFont(this, attrs, defStyleAttr, defStyleRes);
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void applyStyle(int resId){
+ ViewUtil.applyStyle(this, resId);
+ applyStyle(getContext(), null, 0, resId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setTextAppearance(int resId) {
+ ViewUtil.applyTextAppearance(this, resId);
+ }
+
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ RippleManager.cancelRipple(this);
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ @Override
+ public void setBackground(Drawable drawable) {
+ Drawable background = getBackground();
+ if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
+ ((RippleDrawable) background).setBackgroundDrawable(drawable);
+ else
+ super.setBackground(drawable);
+ }
+
+ protected RippleManager getRippleManager(){
+ if(mRippleManager == null){
+ synchronized (RippleManager.class){
+ if(mRippleManager == null)
+ mRippleManager = new RippleManager();
+ }
+ }
+
+ return mRippleManager;
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ RippleManager rippleManager = getRippleManager();
+ if (l == rippleManager)
+ super.setOnClickListener(l);
+ else {
+ rippleManager.setOnClickListener(l);
+ setOnClickListener(rippleManager);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ boolean result = super.onTouchEvent(event);
+ return getRippleManager().onTouchEvent(this, event) || result;
+ }
+
+ public void setOnSelectionChangedListener(OnSelectionChangedListener listener){
+ mOnSelectionChangedListener = listener;
+ }
+
+ @Override
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ super.onSelectionChanged(selStart, selEnd);
+
+ if(mOnSelectionChangedListener != null)
+ mOnSelectionChangedListener.onSelectionChanged(this, selStart, selEnd);
+ }
+}
diff --git a/material/src/main/java/com/rey/material/widget/TimePicker.java b/material/src/main/java/com/rey/material/widget/TimePicker.java
new file mode 100644
index 0000000..6227d08
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/TimePicker.java
@@ -0,0 +1,895 @@
+package com.rey.material.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.rey.material.R;
+import com.rey.material.util.ThemeManager;
+import com.rey.material.util.ColorUtil;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.TypefaceUtil;
+import com.rey.material.util.ViewUtil;
+
+public class TimePicker extends View implements ThemeManager.OnThemeChangedListener{
+
+ protected int mStyleId;
+ protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;
+
+ private int mBackgroundColor;
+ private int mSelectionColor;
+ private int mSelectionRadius = -1;
+ private int mTickSize = -1;
+ private Typeface mTypeface = Typeface.DEFAULT;
+ private int mTextSize = -1;
+ private int mTextColor = 0xFF000000;
+ private int mTextHighlightColor = 0xFFFFFFFF;
+ private boolean m24Hour = true;
+
+ private int mAnimDuration = -1;
+ private Interpolator mInInterpolator;
+ private Interpolator mOutInterpolator;
+ private long mStartTime;
+ private float mAnimProgress;
+ private boolean mRunning;
+
+ private Paint mPaint;
+
+ private PointF mCenterPoint;
+ private float mOuterRadius;
+ private float mInnerRadius;
+ private float mSecondInnerRadius;
+
+ private float[] mLocations = new float[72];
+ private Rect mRect;
+ private String[] mTicks;
+
+ private int mMode = MODE_HOUR;
+
+ public static final int MODE_HOUR = 0;
+ public static final int MODE_MINUTE = 1;
+
+ private int mHour = 0;
+ private int mMinute = 0;
+
+ private boolean mEdited = false;
+
+ /**
+ * Interface definition for a callback to be invoked when the selected time is changed.
+ */
+ public interface OnTimeChangedListener{
+
+ /**
+ * Called when the select mode is changed
+ * @param mode The current mode. Can be {@link #MODE_HOUR} or {@link #MODE_MINUTE}.
+ */
+ void onModeChanged(int mode);
+
+ /**
+ * Called then the selected hour is changed.
+ * @param oldValue The old hour value.
+ * @param newValue The new hour value.
+ */
+ void onHourChanged(int oldValue, int newValue);
+
+ /**
+ * Called then the selected minute is changed.
+ * @param oldValue The old minute value.
+ * @param newValue The new minute value.
+ */
+ void onMinuteChanged(int oldValue, int newValue);
+ }
+
+ private OnTimeChangedListener mOnTimeChangedListener;
+
+ public TimePicker(Context context) {
+ super(context);
+
+ init(context, null, 0, 0);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context, attrs, 0, 0);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mRect = new Rect();
+
+ mBackgroundColor = ColorUtil.getColor(ThemeUtil.colorPrimary(context, 0xFF000000), 0.25f);
+ mSelectionColor = ThemeUtil.colorPrimary(context, 0xFF000000);
+
+ initTickLabels();
+
+ setWillNotDraw(false);
+ applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ if(!isInEditMode())
+ mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Init the localized label of ticks. The value of ticks in order:
+ * 1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12",
+ * "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "0",
+ * "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "0"
+ */
+ private void initTickLabels(){
+ String format = "%2d";
+ mTicks = new String[36];
+ for(int i = 0; i < 23; i++)
+ mTicks[i] = String.format(format, i + 1);
+ mTicks[23] = String.format(format, 0);
+ mTicks[35] = mTicks[23];
+ for(int i = 24; i < 35; i++)
+ mTicks[i] = String.format(format, (i - 23) * 5);
+ }
+
+ public void applyStyle(int styleId){
+ ViewUtil.applyStyle(this, styleId);
+ applyStyle(getContext(), null, 0, styleId);
+ }
+
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
+
+ boolean hourDefined = false;
+ String familyName = null;
+ int style = -1;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+
+ if(attr == R.styleable.TimePicker_tp_backgroundColor)
+ mBackgroundColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_selectionColor)
+ mSelectionColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_selectionRadius)
+ mSelectionRadius = a.getDimensionPixelOffset(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_tickSize)
+ mTickSize = a.getDimensionPixelOffset(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_textSize)
+ mTextSize = a.getDimensionPixelOffset(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_textColor)
+ mTextColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_textHighlightColor)
+ mTextHighlightColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_animDuration)
+ mAnimDuration = a.getInteger(attr, 0);
+ else if(attr == R.styleable.TimePicker_tp_inInterpolator)
+ mInInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0));
+ else if(attr == R.styleable.TimePicker_tp_outInterpolator)
+ mOutInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0));
+ else if(attr == R.styleable.TimePicker_tp_mode)
+ setMode(a.getInteger(attr, 0), false);
+ else if(attr == R.styleable.TimePicker_tp_24Hour) {
+ set24Hour(a.getBoolean(attr, false));
+ hourDefined = true;
+ }
+ else if(attr == R.styleable.TimePicker_tp_hour)
+ setHour(a.getInteger(attr, 0));
+ else if(attr == R.styleable.TimePicker_tp_minute)
+ setMinute(a.getInteger(attr, 0));
+ else if(attr == R.styleable.TimePicker_tp_fontFamily)
+ familyName = a.getString(attr);
+ else if(attr == R.styleable.TimePicker_tp_textStyle)
+ style = a.getInteger(attr, 0);
+ }
+
+ a.recycle();
+
+ if(mSelectionRadius < 0)
+ mSecondInnerRadius = ThemeUtil.dpToPx(context, 8);
+
+ if(mTickSize < 0)
+ mTickSize = ThemeUtil.dpToPx(context, 1);
+
+ if(mTextSize < 0)
+ mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_caption_material);
+
+ if(mAnimDuration < 0)
+ mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+
+ if(mInInterpolator == null)
+ mInInterpolator = new DecelerateInterpolator();
+
+ if(mOutInterpolator == null)
+ mOutInterpolator = new DecelerateInterpolator();
+
+ if(!hourDefined)
+ set24Hour(DateFormat.is24HourFormat(context));
+
+ if(familyName != null || style >= 0)
+ mTypeface = TypefaceUtil.load(context, familyName, style);
+ }
+
+ @Override
+ public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
+ int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
+ if(mCurrentStyle != style){
+ mCurrentStyle = style;
+ applyStyle(mCurrentStyle);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if(mStyleId != 0) {
+ ThemeManager.getInstance().registerOnThemeChangedListener(this);
+ onThemeChanged(null);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if(mStyleId != 0)
+ ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
+ }
+
+ public int getBackgroundColor(){
+ return mBackgroundColor;
+ }
+
+ public int getSelectionColor(){
+ return mSelectionColor;
+ }
+
+ public Typeface getTypeface(){
+ return mTypeface;
+ }
+
+ public int getTextSize(){
+ return mTextSize;
+ }
+
+ public int getTextColor(){
+ return mTextColor;
+ }
+
+ public int getTextHighlightColor(){
+ return mTextHighlightColor;
+ }
+
+ public int getAnimDuration(){
+ return mAnimDuration;
+ }
+
+ public Interpolator getInInterpolator(){
+ return mInInterpolator;
+ }
+
+ public Interpolator getOutInterpolator(){
+ return mOutInterpolator;
+ }
+
+ /**
+ * @return The current select mode. Can be {@link #MODE_HOUR} or {@link #MODE_MINUTE}.
+ */
+ public int getMode(){
+ return mMode;
+ }
+
+ /**
+ * @return The selected hour value.
+ */
+ public int getHour(){
+ return mHour;
+ }
+
+ /**
+ * @return The selected minute value.
+ */
+ public int getMinute(){
+ return mMinute;
+ }
+
+ /**
+ * @return this TimePicker use 24-hour format or not.
+ */
+ public boolean is24Hour(){
+ return m24Hour;
+ }
+
+ /**
+ * Set the select mode of this TimePicker.
+ * @param mode The select mode. Can be {@link #MODE_HOUR} or {@link #MODE_MINUTE}.
+ * @param animation Indicate that should show animation when switch select mode or not.
+ */
+ public void setMode(int mode, boolean animation){
+ if(mMode != mode){
+ mMode = mode;
+
+ if(mOnTimeChangedListener != null)
+ mOnTimeChangedListener.onModeChanged(mMode);
+
+ if(animation)
+ startAnimation();
+ else
+ invalidate();
+ }
+ }
+
+ /**
+ * Set the selected hour value.
+ * @param hour The selected hour value.
+ */
+ public void setHour(int hour){
+ if(m24Hour)
+ hour = Math.max(hour, 0) % 24;
+ else
+ hour = Math.max(hour, 0) % 12;
+
+ if(mHour != hour){
+ int old = mHour;
+ mHour = hour;
+
+ if(mOnTimeChangedListener != null)
+ mOnTimeChangedListener.onHourChanged(old, mHour);
+
+ if(mMode == MODE_HOUR)
+ invalidate();
+ }
+ }
+
+ /**
+ * Set the selected minute value.
+ * @param minute The selected minute value.
+ */
+ public void setMinute(int minute){
+ minute = Math.min(Math.max(minute, 0), 59);
+
+ if(mMinute != minute){
+ int old = mMinute;
+ mMinute = minute;
+
+ if(mOnTimeChangedListener != null)
+ mOnTimeChangedListener.onMinuteChanged(old, mMinute);
+
+ if(mMode == MODE_MINUTE)
+ invalidate();
+ }
+ }
+
+ /**
+ * Set a listener will be called when the selected time is changed.
+ * @param listener The {@link OnTimeChangedListener} will be called.
+ */
+ public void setOnTimeChangedListener(OnTimeChangedListener listener){
+ mOnTimeChangedListener = listener;
+ }
+
+ /**
+ * Set this TimePicker use 24-hour format or not.
+ * @param b b
+ */
+ public void set24Hour(boolean b){
+ if(m24Hour != b){
+ m24Hour = b;
+ if(!m24Hour && mHour > 11)
+ setHour(mHour - 12);
+ calculateTextLocation();
+ }
+ }
+
+ private float getAngle(int value, int mode){
+ switch (mode){
+ case MODE_HOUR:
+ return (float)(-Math.PI / 2 + Math.PI / 6 * value);
+ case MODE_MINUTE:
+ return (float)(-Math.PI / 2 + Math.PI / 30 * value);
+ default:
+ return 0f;
+ }
+ }
+
+ private int getSelectedTick(int value, int mode){
+ switch (mode){
+ case MODE_HOUR:
+ return value == 0 ? (m24Hour ? 23 : 11) : value - 1;
+ case MODE_MINUTE:
+ if(value % 5 == 0)
+ return (value == 0) ? 35 : (value / 5 + 23);
+ default:
+ return -1;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = (widthMode == MeasureSpec.UNSPECIFIED) ? mSelectionRadius * 12 : MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = (heightMode == MeasureSpec.UNSPECIFIED) ? mSelectionRadius * 12 : MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
+
+ int size = Math.min(widthSize, heightSize);
+
+ int width = (widthMode == MeasureSpec.EXACTLY) ? widthSize : size;
+ int height = (heightMode == MeasureSpec.EXACTLY) ? heightSize : size;
+
+ setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
+ }
+
+ private void calculateTextLocation(){
+ if(mCenterPoint == null)
+ return;
+
+ double step = Math.PI / 6;
+ double angle = -Math.PI / 3;
+ float x, y;
+
+ mPaint.setTextSize(mTextSize);
+ mPaint.setTypeface(mTypeface);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+
+ if(m24Hour){
+ for(int i = 0; i < 12; i++){
+ mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect);
+ if(i == 0)
+ mSecondInnerRadius = mInnerRadius - mSelectionRadius - mRect.height();
+
+ x = mCenterPoint.x + (float)Math.cos(angle) * mSecondInnerRadius;
+ y = mCenterPoint.y + (float)Math.sin(angle) * mSecondInnerRadius;
+
+ mLocations[i * 2] = x;
+ mLocations[i * 2 + 1] = y + mRect.height() / 2f;
+
+ angle += step;
+ }
+
+ for(int i = 12; i < mTicks.length; i++){
+ x = mCenterPoint.x + (float)Math.cos(angle) * mInnerRadius;
+ y = mCenterPoint.y + (float)Math.sin(angle) * mInnerRadius;
+
+ mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect);
+ mLocations[i * 2] = x;
+ mLocations[i * 2 + 1] = y + mRect.height() / 2f;
+
+ angle += step;
+ }
+ }
+ else{
+ for(int i = 0; i < 12; i++){
+ x = mCenterPoint.x + (float)Math.cos(angle) * mInnerRadius;
+ y = mCenterPoint.y + (float)Math.sin(angle) * mInnerRadius;
+
+ mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect);
+ mLocations[i * 2] = x;
+ mLocations[i * 2 + 1] = y + mRect.height() / 2f;
+
+ angle += step;
+ }
+
+ for(int i = 24; i < mTicks.length; i++){
+ x = mCenterPoint.x + (float)Math.cos(angle) * mInnerRadius;
+ y = mCenterPoint.y + (float)Math.sin(angle) * mInnerRadius;
+
+ mPaint.getTextBounds(mTicks[i], 0, mTicks[i].length(), mRect);
+ mLocations[i * 2] = x;
+ mLocations[i * 2 + 1] = y + mRect.height() / 2f;
+
+ angle += step;
+ }
+ }
+
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ int left = getPaddingLeft();
+ int top = getPaddingTop();
+ int size = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom());
+
+ if(mCenterPoint == null)
+ mCenterPoint = new PointF();
+
+ mOuterRadius = size / 2f;
+ mCenterPoint.set(left + mOuterRadius, top + mOuterRadius);
+ mInnerRadius = mOuterRadius - mSelectionRadius - ThemeUtil.dpToPx(getContext(), 4);
+
+ calculateTextLocation();
+ }
+
+ private int getPointedValue(float x, float y, boolean isDown){
+ float radius = (float) Math.sqrt(Math.pow(x - mCenterPoint.x, 2) + Math.pow(y - mCenterPoint.y, 2));
+ if(isDown) {
+ if(mMode == MODE_HOUR && m24Hour){
+ if (radius > mInnerRadius + mSelectionRadius || radius < mSecondInnerRadius - mSelectionRadius)
+ return -1;
+ }
+ else if (radius > mInnerRadius + mSelectionRadius || radius < mInnerRadius - mSelectionRadius)
+ return -1;
+ }
+
+ float angle = (float)Math.atan2(y - mCenterPoint.y, x - mCenterPoint.x);
+ if(angle < 0)
+ angle += Math.PI * 2;
+
+ if(mMode == MODE_HOUR){
+ if(m24Hour){
+ if(radius > mSecondInnerRadius + mSelectionRadius / 2){
+ int value = (int) Math.round(angle * 6 / Math.PI) + 15;
+ if(value == 24)
+ return 0;
+ else if(value > 24)
+ return value - 12;
+ else
+ return value;
+ }
+ else{
+ int value = (int) Math.round(angle * 6 / Math.PI) + 3;
+ return value > 12 ? value - 12 : value;
+ }
+ }
+ else {
+ int value = (int) Math.round(angle * 6 / Math.PI) + 3;
+ return value > 11 ? value - 12 : value;
+ }
+ }
+ else if(mMode == MODE_MINUTE){
+ int value = (int)Math.round(angle * 30 / Math.PI) + 15;
+ return value > 59 ? value - 60 : value;
+ }
+
+ return -1;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()){
+ case MotionEvent.ACTION_DOWN:
+ int value = getPointedValue(event.getX(), event.getY(), true);
+ if(value < 0)
+ return false;
+ else if(mMode == MODE_HOUR)
+ setHour(value);
+ else if(mMode == MODE_MINUTE)
+ setMinute(value);
+ mEdited = true;
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ value = getPointedValue(event.getX(), event.getY(), false);
+ if(value < 0)
+ return true;
+ else if(mMode == MODE_HOUR)
+ setHour(value);
+ else if(mMode == MODE_MINUTE)
+ setMinute(value);
+ mEdited = true;
+ return true;
+ case MotionEvent.ACTION_UP:
+ if(mEdited && mMode == MODE_HOUR){
+ setMode(MODE_MINUTE, true);
+ mEdited = false;
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mEdited = false;
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ mPaint.setColor(mBackgroundColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mOuterRadius, mPaint);
+
+ if(!mRunning){
+ float angle;
+ int selectedTick;
+ int start;
+ int length;
+ float radius;
+
+ if(mMode == MODE_HOUR){
+ angle = getAngle(mHour, MODE_HOUR);
+ selectedTick = getSelectedTick(mHour, MODE_HOUR);
+ start = 0;
+ length = m24Hour ? 24 : 12;
+ radius = m24Hour && selectedTick < 12 ? mSecondInnerRadius : mInnerRadius;
+ }
+ else{
+ angle = getAngle(mMinute, MODE_MINUTE);
+ selectedTick = getSelectedTick(mMinute, MODE_MINUTE);
+ start = 24;
+ length = 12;
+ radius = mInnerRadius;
+ }
+
+ mPaint.setColor(mSelectionColor);
+ float x = mCenterPoint.x + (float)Math.cos(angle) * radius;
+ float y = mCenterPoint.y + (float)Math.sin(angle) * radius;
+ canvas.drawCircle(x, y, mSelectionRadius, mPaint);
+
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mTickSize);
+ x -= (float)Math.cos(angle) * mSelectionRadius;
+ y -= (float)Math.sin(angle) * mSelectionRadius;
+ canvas.drawLine(mCenterPoint.x, mCenterPoint.y, x, y, mPaint);
+
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(mTextColor);
+ canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mTickSize * 2, mPaint);
+
+ mPaint.setTextSize(mTextSize);
+ mPaint.setTypeface(mTypeface);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+
+ int index;
+ for(int i = 0; i < length; i++) {
+ index = start + i;
+ mPaint.setColor(index == selectedTick ? mTextHighlightColor : mTextColor);
+ canvas.drawText(mTicks[index], mLocations[index * 2], mLocations[index * 2 + 1], mPaint);
+ }
+ }
+ else{
+ float maxOffset = mOuterRadius - mInnerRadius + mTextSize / 2;
+ int textOutColor = ColorUtil.getColor(mTextColor, 1f - mAnimProgress);
+ int textHighlightOutColor= ColorUtil.getColor(mTextHighlightColor, 1f - mAnimProgress);
+ int textInColor = ColorUtil.getColor(mTextColor, mAnimProgress);
+ int textHighlightInColor= ColorUtil.getColor(mTextHighlightColor, mAnimProgress);
+ float outOffset;
+ float inOffset;
+ float outAngle;
+ float inAngle;
+ int outStart;
+ int inStart;
+ int outLength;
+ int inLength;
+ int outSelectedTick;
+ int inSelectedTick;
+ float outRadius;
+ float inRadius;
+
+ if(mMode == MODE_MINUTE){
+ outAngle = getAngle(mHour, MODE_HOUR);
+ inAngle = getAngle(mMinute, MODE_MINUTE);
+ outOffset = mOutInterpolator.getInterpolation(mAnimProgress) * maxOffset;
+ inOffset = (1f - mInInterpolator.getInterpolation(mAnimProgress)) * -maxOffset;
+ outSelectedTick = getSelectedTick(mHour, MODE_HOUR);
+ inSelectedTick = getSelectedTick(mMinute, MODE_MINUTE);
+ outStart = 0;
+ outLength = m24Hour ? 24 : 12;
+ outRadius = m24Hour && outSelectedTick < 12 ? mSecondInnerRadius : mInnerRadius;
+ inStart = 24;
+ inLength = 12;
+ inRadius = mInnerRadius;
+ }
+ else{
+ outAngle = getAngle(mMinute, MODE_MINUTE);
+ inAngle = getAngle(mHour, MODE_HOUR);
+ outOffset = mOutInterpolator.getInterpolation(mAnimProgress) * -maxOffset;
+ inOffset = (1f - mInInterpolator.getInterpolation(mAnimProgress)) * maxOffset;
+ outSelectedTick = getSelectedTick(mMinute, MODE_MINUTE);
+ inSelectedTick = getSelectedTick(mHour, MODE_HOUR);
+ outStart = 24;
+ outLength = 12;
+ outRadius = mInnerRadius;
+ inStart = 0;
+ inLength = m24Hour ? 24 : 12;
+ inRadius = m24Hour && inSelectedTick < 12 ? mSecondInnerRadius : mInnerRadius;
+ }
+
+ mPaint.setColor(ColorUtil.getColor(mSelectionColor, 1f - mAnimProgress));
+ float x = mCenterPoint.x + (float)Math.cos(outAngle) * (outRadius + outOffset);
+ float y = mCenterPoint.y + (float)Math.sin(outAngle) * (outRadius + outOffset);
+ canvas.drawCircle(x, y, mSelectionRadius, mPaint);
+
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mTickSize);
+ x -= (float)Math.cos(outAngle) * mSelectionRadius;
+ y -= (float)Math.sin(outAngle) * mSelectionRadius;
+ canvas.drawLine(mCenterPoint.x, mCenterPoint.y, x, y, mPaint);
+
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(ColorUtil.getColor(mSelectionColor, mAnimProgress));
+ x = mCenterPoint.x + (float)Math.cos(inAngle) * (inRadius + inOffset);
+ y = mCenterPoint.y + (float)Math.sin(inAngle) * (inRadius + inOffset);
+ canvas.drawCircle(x, y, mSelectionRadius, mPaint);
+
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(mTickSize);
+ x -= (float)Math.cos(inAngle) * mSelectionRadius;
+ y -= (float)Math.sin(inAngle) * mSelectionRadius;
+ canvas.drawLine(mCenterPoint.x, mCenterPoint.y, x, y, mPaint);
+
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setColor(mTextColor);
+ canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mTickSize * 2, mPaint);
+
+ mPaint.setTextSize(mTextSize);
+ mPaint.setTypeface(mTypeface);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+
+ double step = Math.PI / 6;
+ double angle = -Math.PI / 3;
+ int index;
+
+ for(int i = 0; i < outLength; i++){
+ index = i + outStart;
+ x = mLocations[index * 2] + (float)Math.cos(angle) * outOffset;
+ y = mLocations[index * 2 + 1] + (float)Math.sin(angle) * outOffset;
+ mPaint.setColor(index == outSelectedTick ? textHighlightOutColor : textOutColor);
+ canvas.drawText(mTicks[index], x, y, mPaint);
+ angle += step;
+ }
+
+ for(int i = 0; i < inLength; i++){
+ index = i + inStart;
+ x = mLocations[index * 2] + (float)Math.cos(angle) * inOffset;
+ y = mLocations[index * 2 + 1] + (float)Math.sin(angle) * inOffset;
+ mPaint.setColor(index == inSelectedTick ? textHighlightInColor : textInColor);
+ canvas.drawText(mTicks[index], x, y, mPaint);
+ angle += step;
+ }
+ }
+ }
+
+ private void resetAnimation(){
+ mStartTime = SystemClock.uptimeMillis();
+ mAnimProgress = 0f;
+ }
+
+ private void startAnimation() {
+ if(getHandler() != null){
+ resetAnimation();
+ mRunning = true;
+ getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ }
+
+ invalidate();
+ }
+
+ private void stopAnimation() {
+ mRunning = false;
+ mAnimProgress = 1f;
+ if(getHandler() != null)
+ getHandler().removeCallbacks(mUpdater);
+ invalidate();
+ }
+
+ private final Runnable mUpdater = new Runnable() {
+
+ @Override
+ public void run() {
+ update();
+ }
+
+ };
+
+ private void update(){
+ long curTime = SystemClock.uptimeMillis();
+ mAnimProgress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration);
+
+ if(mAnimProgress == 1f)
+ stopAnimation();
+
+ if(mRunning) {
+ if(getHandler() != null)
+ getHandler().postAtTime(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
+ else
+ stopAnimation();
+ }
+
+ invalidate();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ ss.mode = mMode;
+ ss.hour = mHour;
+ ss.minute = mMinute;
+ ss.is24Hour = m24Hour;
+
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ set24Hour(ss.is24Hour);
+ setMode(ss.mode, false);
+ setHour(ss.hour);
+ setMinute(ss.minute);
+ }
+
+ static class SavedState extends BaseSavedState {
+ int mode;
+ int hour;
+ int minute;
+ boolean is24Hour;
+
+ /**
+ * Constructor called from {@link Switch#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ mode = in.readInt();
+ hour = in.readInt();
+ minute = in.readInt();
+ is24Hour = in.readInt() == 1;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeValue(mode);
+ out.writeValue(hour);
+ out.writeValue(minute);
+ out.writeValue(is24Hour ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ return "TimePicker.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " mode=" + mode
+ + " hour=" + hour
+ + " minute=" + minute
+ + "24hour=" + is24Hour + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/material/src/main/java/com/rey/material/widget/YearPicker.java b/material/src/main/java/com/rey/material/widget/YearPicker.java
new file mode 100644
index 0000000..26c7676
--- /dev/null
+++ b/material/src/main/java/com/rey/material/widget/YearPicker.java
@@ -0,0 +1,474 @@
+package com.rey.material.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.BaseAdapter;
+
+import com.rey.material.R;
+import com.rey.material.drawable.BlankDrawable;
+import com.rey.material.util.ThemeUtil;
+import com.rey.material.util.TypefaceUtil;
+
+import java.util.Calendar;
+
+public class YearPicker extends ListView {
+
+ private YearAdapter mAdapter;
+
+ private int mTextSize;
+ private int mItemHeight;
+ private int mSelectionColor;
+ private int mAnimDuration;
+ private Interpolator mInInterpolator;
+ private Interpolator mOutInterpolator;
+ private Typeface mTypeface;
+
+ private int mItemRealHeight;
+ private int mPadding;
+ private int mPositionShift;
+ private int mDistanceShift;
+
+ private Paint mPaint;
+
+ /**
+ * Interface definition for a callback to be invoked when the selected year is changed.
+ */
+ public interface OnYearChangedListener{
+
+ /**
+ * Called then the selected year is changed.
+ * @param oldValue The old year value.
+ * @param newValue The new year value.
+ */
+ void onYearChanged(int oldValue, int newValue);
+
+ }
+
+ private OnYearChangedListener mOnYearChangedListener;
+
+ private static final int[][] STATES = new int[][]{
+ new int[]{-android.R.attr.state_checked},
+ new int[]{android.R.attr.state_checked},
+ };
+
+ private int[] mTextColors = new int[]{0xFF000000, 0xFFFFFFFF};
+
+ private static final String YEAR_FORMAT = "%4d";
+
+ public YearPicker(Context context) {
+ super(context);
+ }
+
+ public YearPicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public YearPicker(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public YearPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ mTextSize = -1;
+ mItemHeight = -1;
+ mAnimDuration = -1;
+ mTypeface = Typeface.DEFAULT;
+ mItemRealHeight = -1;
+
+ setWillNotDraw(false);
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setStyle(Paint.Style.FILL);
+
+ mAdapter = new YearAdapter();
+ setAdapter(mAdapter);
+ setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
+ setSelector(BlankDrawable.getInstance());
+ setDividerHeight(0);
+ setCacheColorHint(Color.TRANSPARENT);
+ setClipToPadding(false);
+
+ mPadding = ThemeUtil.dpToPx(context, 4);
+
+ mSelectionColor = ThemeUtil.colorPrimary(context, 0xFF000000);
+
+ super.init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
+ super.applyStyle(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.YearPicker, defStyleAttr, defStyleRes);
+
+ int year = -1;
+ int yearMin = -1;
+ int yearMax = -1;
+ String familyName = null;
+ int style = -1;
+
+ for(int i = 0, count = a.getIndexCount(); i < count; i++){
+ int attr = a.getIndex(i);
+
+ if(attr == R.styleable.YearPicker_dp_yearTextSize)
+ mTextSize = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_year)
+ year = a.getInteger(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_yearMin)
+ yearMin = a.getInteger(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_yearMax)
+ yearMax = a.getInteger(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_yearItemHeight)
+ mItemHeight = a.getDimensionPixelSize(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_textColor)
+ mTextColors[0] = a.getColor(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_textHighlightColor)
+ mTextColors[1] = a.getColor(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_selectionColor)
+ mSelectionColor = a.getColor(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_animDuration)
+ mAnimDuration = a.getInteger(attr, 0);
+ else if(attr == R.styleable.YearPicker_dp_inInterpolator)
+ mInInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0));
+ else if(attr == R.styleable.YearPicker_dp_outInterpolator)
+ mOutInterpolator = AnimationUtils.loadInterpolator(context, a.getResourceId(attr, 0));
+ else if(attr == R.styleable.YearPicker_dp_fontFamily)
+ familyName = a.getString(attr);
+ else if(attr == R.styleable.YearPicker_dp_textStyle)
+ style = a.getInteger(attr, 0);
+ }
+
+ a.recycle();
+
+ if(mTextSize < 0)
+ mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_title_material);
+
+ if(mItemHeight < 0)
+ mItemHeight = ThemeUtil.dpToPx(context, 48);
+
+ if(mAnimDuration < 0)
+ mAnimDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+
+ if(mInInterpolator == null)
+ mInInterpolator = new DecelerateInterpolator();
+
+ if(mOutInterpolator == null)
+ mOutInterpolator = new DecelerateInterpolator();
+
+ if(familyName != null || style >= 0)
+ mTypeface = TypefaceUtil.load(context, familyName, style);
+
+ if(yearMin >= 0 || yearMax >= 0){
+ if(yearMin < 0)
+ yearMin = mAdapter.getMinYear();
+
+ if(yearMax < 0)
+ yearMax = mAdapter.getMaxYear();
+
+ if(yearMax < yearMin)
+ yearMax = Integer.MAX_VALUE;
+
+ setYearRange(yearMin, yearMax);
+ }
+
+ if(mAdapter.getYear() < 0 && year < 0){
+ Calendar cal = Calendar.getInstance();
+ year = cal.get(Calendar.YEAR);
+ }
+
+ if(year >= 0){
+ year = Math.max(yearMin, Math.min(yearMax, year));
+ setYear(year);
+ }
+
+ mAdapter.notifyDataSetChanged();
+ requestLayout();
+ }
+
+ /**
+ * Set the range of selectable year value.
+ * @param min The minimum selectable year value.
+ * @param max The maximum selectable year value.
+ */
+ public void setYearRange(int min, int max){
+ mAdapter.setYearRange(min, max);
+ }
+
+ /**
+ * Jump to a specific year.
+ * @param year y
+ */
+ public void goTo(int year){
+ int position = mAdapter.positionOfYear(year) - mPositionShift;
+ int offset = mDistanceShift;
+ if(position < 0){
+ position = 0;
+ offset = 0;
+ }
+ postSetSelectionFromTop(position, offset);
+ }
+
+ public void postSetSelectionFromTop(final int position, final int offset) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ setSelectionFromTop(position, offset);
+ requestLayout();
+ }
+ });
+ }
+
+ /**
+ * Set the selected year.
+ * @param year The selected year value.
+ */
+ public void setYear(int year){
+ if(mAdapter.getYear() == year)
+ return;
+
+ mAdapter.setYear(year);
+ goTo(year);
+ }
+
+ /**
+ * @return The selected year value.
+ */
+ public int getYear(){
+ return mAdapter.getYear();
+ }
+
+ /**
+ * Set a listener will be called when the selected year value is changed.
+ * @param listener The {@link OnYearChangedListener} will be called.
+ */
+ public void setOnYearChangedListener(OnYearChangedListener listener){
+ mOnYearChangedListener = listener;
+ }
+
+ private void measureItemHeight(){
+ if(mItemRealHeight > 0)
+ return;
+
+ mPaint.setTextSize(mTextSize);
+ mItemRealHeight = Math.max(Math.round(mPaint.measureText("9999", 0, 4)) + mPadding * 2, mItemHeight);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ measureItemHeight();
+
+ if(heightMode != MeasureSpec.EXACTLY){
+ if(heightMode == MeasureSpec.AT_MOST){
+ int num = Math.min(mAdapter.getCount(), heightSize / mItemRealHeight);
+ if(num >= 3)
+ heightSize = mItemRealHeight * (num % 2 == 0 ? num - 1 : num);
+ }
+ else
+ heightSize = mItemRealHeight * mAdapter.getCount();
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY);
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldWidth, int oldHigh) {
+ float shift = (h / (float)mItemRealHeight - 1) / 2;
+ mPositionShift = (int)Math.floor(shift);
+ mPositionShift = shift > mPositionShift ? mPositionShift + 1 : mPositionShift;
+ mDistanceShift = (int)((shift - mPositionShift) * mItemRealHeight) - getPaddingTop();
+ goTo(mAdapter.getYear());
+ }
+
+ private class YearAdapter extends BaseAdapter implements OnClickListener{
+
+ private int mMinYear = 1990;
+ private int mMaxYear = Integer.MAX_VALUE - 1;
+ private int mCurYear = -1;
+
+ public YearAdapter(){}
+
+ public int getMinYear(){
+ return mMinYear;
+ }
+
+ public int getMaxYear(){
+ return mMaxYear;
+ }
+
+ public void setYearRange(int min, int max){
+ if(mMinYear != min || mMaxYear != max){
+ mMinYear = min;
+ mMaxYear = max;
+ notifyDataSetChanged();
+ }
+ }
+
+ public int positionOfYear(int year){
+ return year - mMinYear;
+ }
+
+ @Override
+ public int getCount(){
+ return mMaxYear - mMinYear + 1;
+ }
+
+ @Override
+ public Object getItem(int position){
+ return mMinYear + position;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ public void setYear(int year){
+ if(mCurYear != year){
+ int old = mCurYear;
+ mCurYear = year;
+
+ CircleCheckedTextView child = (CircleCheckedTextView) YearPicker.this.getChildAt(positionOfYear(old) - YearPicker.this.getFirstVisiblePosition());
+ if(child != null)
+ child.setChecked(false);
+
+ child = (CircleCheckedTextView) YearPicker.this.getChildAt(positionOfYear(mCurYear) - YearPicker.this.getFirstVisiblePosition());
+ if(child != null)
+ child.setChecked(true);
+
+ if(mOnYearChangedListener != null)
+ mOnYearChangedListener.onYearChanged(old, mCurYear);
+ }
+ }
+
+ public int getYear(){
+ return mCurYear;
+ }
+
+ @Override
+ public void onClick(View v) {
+ setYear((Integer)v.getTag());
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ CircleCheckedTextView v = (CircleCheckedTextView)convertView;
+ if(v == null){
+ v = new CircleCheckedTextView(getContext());
+ v.setGravity(Gravity.CENTER);
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
+ v.setTextAlignment(TEXT_ALIGNMENT_CENTER);
+ v.setMinHeight(mItemRealHeight);
+ v.setMaxHeight(mItemRealHeight);
+ v.setAnimDuration(mAnimDuration);
+ v.setInterpolator(mInInterpolator, mOutInterpolator);
+ v.setBackgroundColor(mSelectionColor);
+ v.setTypeface(mTypeface);
+ v.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ v.setTextColor(new ColorStateList(STATES, mTextColors));
+ v.setOnClickListener(this);
+ }
+
+ int year = (Integer)getItem(position);
+ v.setTag(year);
+ v.setText(String.format(YEAR_FORMAT, year));
+ v.setCheckedImmediately(year == mCurYear);
+ return v;
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ ss.yearMin = mAdapter.getMinYear();
+ ss.yearMax = mAdapter.getMaxYear();
+ ss.year = mAdapter.getYear();
+
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ setYearRange(ss.yearMin, ss.yearMax);
+ setYear(ss.year);
+ }
+
+ static class SavedState extends BaseSavedState {
+ int yearMin;
+ int yearMax;
+ int year;
+
+ /**
+ * Constructor called from {@link Switch#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ yearMin = in.readInt();
+ yearMax = in.readInt();
+ year = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeValue(yearMin);
+ out.writeValue(yearMax);
+ out.writeValue(year);
+ }
+
+ @Override
+ public String toString() {
+ return "YearPicker.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " yearMin=" + yearMin
+ + " yearMax=" + yearMax
+ + " year=" + year + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/material/src/main/res/anim/anim_scale_in.xml b/material/src/main/res/anim/anim_scale_in.xml
new file mode 100644
index 0000000..65b2ac9
--- /dev/null
+++ b/material/src/main/res/anim/anim_scale_in.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/material/src/main/res/anim/anim_scale_out.xml b/material/src/main/res/anim/anim_scale_out.xml
new file mode 100644
index 0000000..50df398
--- /dev/null
+++ b/material/src/main/res/anim/anim_scale_out.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/material/src/main/res/drawable/selector.xml b/material/src/main/res/drawable/selector.xml
new file mode 100644
index 0000000..88f81ce
--- /dev/null
+++ b/material/src/main/res/drawable/selector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/material/src/main/res/values/attrs.xml b/material/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..6ef6b29
--- /dev/null
+++ b/material/src/main/res/values/attrs.xml
@@ -0,0 +1,682 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/material/src/main/res/values/colors.xml b/material/src/main/res/values/colors.xml
new file mode 100644
index 0000000..7594b7a
--- /dev/null
+++ b/material/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #00FFFFFF
+
\ No newline at end of file
diff --git a/material/src/main/res/values/drawables.xml b/material/src/main/res/values/drawables.xml
new file mode 100644
index 0000000..25a8a3b
--- /dev/null
+++ b/material/src/main/res/values/drawables.xml
@@ -0,0 +1,5 @@
+
+
+ #00FFFFFF
+ #100F0000
+
\ No newline at end of file
diff --git a/material/src/main/res/values/styles.xml b/material/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cefb601
--- /dev/null
+++ b/material/src/main/res/values/styles.xml
@@ -0,0 +1,580 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/material/src/main/res/xml/nav_states.xml b/material/src/main/res/xml/nav_states.xml
new file mode 100644
index 0000000..1afc63e
--- /dev/null
+++ b/material/src/main/res/xml/nav_states.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ - 0
+ - 0.1
+ - 1
+ - 0.1
+ - 0
+ - 0.5
+ - 1
+ - 0.5
+ - 0
+ - 0.9
+ - 1
+ - 0.9
+
+
+
+
+
+ - 0.5
+ - 0
+ - 1
+ - 0.5
+ - 0
+ - 0.5
+ - 1
+ - 0.5
+ - 0.5
+ - 1
+ - 1
+ - 0.5
+
+
+
+ - 0
+ - 2
+
+
+
+
diff --git a/material/src/test/java/com/rey/material/ExampleUnitTest.java b/material/src/test/java/com/rey/material/ExampleUnitTest.java
new file mode 100644
index 0000000..64a6ebe
--- /dev/null
+++ b/material/src/test/java/com/rey/material/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package com.rey.material;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/mdpreference/.gitignore b/mdpreference/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/mdpreference/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/mdpreference/build.gradle b/mdpreference/build.gradle
new file mode 100644
index 0000000..fb78d58
--- /dev/null
+++ b/mdpreference/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion '23.0.2'
+
+ defaultConfig {
+ minSdkVersion 16
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+}
+
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ compile project(':material')
+}
diff --git a/mdpreference/proguard-rules.pro b/mdpreference/proguard-rules.pro
new file mode 100644
index 0000000..f02a51c
--- /dev/null
+++ b/mdpreference/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\Program Files\adt\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/mdpreference/src/androidTest/java/io/github/xhinliang/mdpreference/ApplicationTest.java b/mdpreference/src/androidTest/java/io/github/xhinliang/mdpreference/ApplicationTest.java
new file mode 100644
index 0000000..f702d9f
--- /dev/null
+++ b/mdpreference/src/androidTest/java/io/github/xhinliang/mdpreference/ApplicationTest.java
@@ -0,0 +1,13 @@
+package io.github.xhinliang.mdpreference;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/mdpreference/src/main/AndroidManifest.xml b/mdpreference/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..708faaa
--- /dev/null
+++ b/mdpreference/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/mdpreference/src/main/assets/fonts/Roboto-Medium.ttf b/mdpreference/src/main/assets/fonts/Roboto-Medium.ttf
new file mode 100644
index 0000000..a3c1a1f
Binary files /dev/null and b/mdpreference/src/main/assets/fonts/Roboto-Medium.ttf differ
diff --git a/mdpreference/src/main/assets/fonts/Roboto-Regular.ttf b/mdpreference/src/main/assets/fonts/Roboto-Regular.ttf
new file mode 100644
index 0000000..0e58508
Binary files /dev/null and b/mdpreference/src/main/assets/fonts/Roboto-Regular.ttf differ
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/CheckBoxPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/CheckBoxPreference.java
new file mode 100644
index 0000000..700d8b0
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/CheckBoxPreference.java
@@ -0,0 +1,42 @@
+package io.github.xhinliang.mdpreference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.rey.material.widget.CheckBox;
+
+public class CheckBoxPreference extends TwoStatePreference {
+ public CheckBoxPreference(Context context) {
+ super(context);
+ init(context, null, 0, 0);
+ }
+
+ public CheckBoxPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0, 0);
+ }
+
+ public CheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ public CheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super.init(context, attrs, defStyleAttr, defStyleRes);
+ setWidgetLayoutResource(R.layout.mp_checkbox_preference);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ CheckBox checkBox = (CheckBox) view.findViewById(R.id.checkable);
+ checkBox.setCheckedImmediately(isChecked());
+ syncSummaryView();
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/DialogPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/DialogPreference.java
new file mode 100644
index 0000000..6d62daa
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/DialogPreference.java
@@ -0,0 +1,179 @@
+package io.github.xhinliang.mdpreference;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.StringRes;
+import android.util.AttributeSet;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.rey.material.dialog.Dialog;
+
+/**
+ * Created by xhinliang on 16-2-23.
+ * DialogPreference
+ */
+public abstract class DialogPreference extends Preference {
+
+ protected CharSequence mDialogTitle;
+ protected CharSequence mDialogMessage;
+ protected CharSequence mPositiveButtonText;
+ protected CharSequence mNegativeButtonText;
+
+ protected Dialog mDialog;
+
+ public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context);
+ }
+
+ private void init(Context context) {
+ mDialogTitle = getTitle();
+ mPositiveButtonText = context.getText(R.string.confirm);
+ mNegativeButtonText = context.getText(R.string.cancel);
+ }
+
+ public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public DialogPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public DialogPreference(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public void setDialogTitle(CharSequence dialogTitle) {
+ mDialogTitle = dialogTitle;
+ }
+
+ public void setDialogTitle(int dialogTitleResId) {
+ setDialogTitle(getContext().getString(dialogTitleResId));
+ }
+
+ public CharSequence getDialogTitle() {
+ return mDialogTitle;
+ }
+
+ public void setDialogMessage(CharSequence dialogMessage) {
+ mDialogMessage = dialogMessage;
+ }
+
+ public CharSequence getDialogMessage() {
+ return mDialogMessage;
+ }
+
+ public void setPositiveButtonText(CharSequence positiveButtonText) {
+ mPositiveButtonText = positiveButtonText;
+ }
+
+ public void setPositiveButtonText(@StringRes int positiveButtonTextResId) {
+ setPositiveButtonText(getContext().getString(positiveButtonTextResId));
+ }
+
+ public void setNegativeButtonText(CharSequence negativeButtonText) {
+ mNegativeButtonText = negativeButtonText;
+ }
+
+ public void setNegativeButtonText(@StringRes int negativeButtonTextResId) {
+ setNegativeButtonText(getContext().getString(negativeButtonTextResId));
+ }
+
+ @Override
+ protected void onClick() {
+ if (mDialog != null && mDialog.isShowing()) return;
+ showDialog(null);
+ }
+
+ protected abstract void onShowDialog(Bundle state);
+
+ private void showDialog(Bundle bundle){
+ onShowDialog(bundle);
+ if (needInputMethod()) {
+ requestInputMethod(mDialog);
+ }
+ }
+
+ protected boolean needInputMethod() {
+ return false;
+ }
+
+ private void requestInputMethod(android.app.Dialog dialog) {
+ Window window = dialog.getWindow();
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ }
+
+ public Dialog getDialog() {
+ return mDialog;
+ }
+
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (mDialog == null || !mDialog.isShowing()) {
+ return superState;
+ }
+
+ final DialogSavedState myState = new DialogSavedState(superState);
+ myState.isDialogShowing = true;
+ myState.dialogBundle = mDialog.onSaveInstanceState();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(DialogSavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ DialogSavedState myState = (DialogSavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ if (myState.isDialogShowing) {
+ showDialog(myState.dialogBundle);
+ }
+ }
+
+ protected static class DialogSavedState extends BaseSavedState {
+ boolean isDialogShowing;
+ Bundle dialogBundle;
+
+ public DialogSavedState(Parcel source) {
+ super(source);
+ isDialogShowing = source.readInt() == 1;
+ dialogBundle = source.readBundle();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(isDialogShowing ? 1 : 0);
+ dest.writeBundle(dialogBundle);
+ }
+
+ public DialogSavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public DialogSavedState createFromParcel(Parcel in) {
+ return new DialogSavedState(in);
+ }
+
+ public DialogSavedState[] newArray(int size) {
+ return new DialogSavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/EditTextPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/EditTextPreference.java
new file mode 100644
index 0000000..9713d87
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/EditTextPreference.java
@@ -0,0 +1,152 @@
+package io.github.xhinliang.mdpreference;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import com.rey.material.dialog.SimpleDialog;
+import com.rey.material.widget.EditText;
+
+/**
+ * Created by xhinliang on 16-2-29.
+ * sf
+ */
+public class EditTextPreference extends DialogPreference {
+ public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public EditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EditTextPreference(Context context) {
+ super(context);
+ }
+
+
+ private String mText;
+
+ public void setText(String text) {
+ final boolean wasBlocking = shouldDisableDependents();
+ mText = text;
+ persistString(text);
+ final boolean isBlocking = shouldDisableDependents();
+ if (isBlocking != wasBlocking) {
+ notifyDependencyChange(isBlocking);
+ }
+ notifyChanged();
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return mText == null ? super.getSummary() : mText;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setText(restoreValue ? getPersistedString(mText) : (String) defaultValue);
+ }
+
+ @Override
+ protected void onShowDialog(Bundle state) {
+ com.rey.material.dialog.Dialog.Builder mBuilder = new SimpleDialog.Builder()
+ .title(mDialogTitle)
+ .contentView(R.layout.mp_edittext)
+ .positiveAction(mPositiveButtonText, new com.rey.material.dialog.Dialog.Action1() {
+ @Override
+ public void onAction(com.rey.material.dialog.Dialog dialog) {
+ String value = ((com.rey.material.widget.EditText) dialog.findViewById(R.id.custom_et)).getText().toString();
+ if (callChangeListener(value)) {
+ setText(value);
+ }
+ }
+ })
+ .negativeAction(mNegativeButtonText, null);
+
+ final Dialog dialog = mDialog = mBuilder.build(getContext());
+ EditText editText = (com.rey.material.widget.EditText) dialog.findViewById(R.id.custom_et);
+ editText.setText(getSummary());
+ editText.setSelection(getSummary().length());
+ if (state != null) {
+ dialog.onRestoreInstanceState(state);
+ }
+ dialog.show();
+ }
+
+ @Override
+ public boolean shouldDisableDependents() {
+ return TextUtils.isEmpty(mText) || super.shouldDisableDependents();
+ }
+
+ @Override
+ protected boolean needInputMethod() {
+ return true;
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ return superState;
+ }
+ final SavedState myState = new SavedState(superState);
+ myState.text = getText();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setText(myState.text);
+ }
+
+
+ private static class SavedState extends BaseSavedState {
+ String text;
+
+ public SavedState(Parcel source) {
+ super(source);
+ text = source.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeString(text);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ListPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ListPreference.java
new file mode 100644
index 0000000..57a46b7
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ListPreference.java
@@ -0,0 +1,216 @@
+package io.github.xhinliang.mdpreference;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.ArrayRes;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import com.rey.material.dialog.SimpleDialog;
+
+/**
+ * Created by xhinliang on 16-2-23.
+ * List
+ */
+public class ListPreference extends DialogPreference {
+
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+ private String mValue;
+ private String mFormat;
+ private int mClickedDialogEntryIndex;
+ private boolean mValueSet;
+
+ public ListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public ListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ public ListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0, 0);
+ }
+
+ public ListPreference(Context context) {
+ super(context);
+ init(context, null, 0, 0);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.list_preference, defStyleAttr, defStyleRes);
+ mEntries = a.getTextArray(R.styleable.list_preference_entry_arr);
+ mEntryValues = a.getTextArray(R.styleable.list_preference_value_arr);
+ mFormat = a.getString(R.styleable.list_preference_format_str);
+ a.recycle();
+ }
+
+ @Override
+ protected void onShowDialog(Bundle state) {
+ if (mEntries == null || mEntryValues == null) {
+ throw new IllegalStateException(
+ "ListPreference requires an entries array and an entryValues array.");
+ }
+
+ mClickedDialogEntryIndex = getValueIndex();
+ com.rey.material.dialog.Dialog.Builder mBuilder = new SimpleDialog.Builder()
+ .items(mEntries, mClickedDialogEntryIndex)
+ .title(mDialogTitle)
+ .positiveAction(mPositiveButtonText, new com.rey.material.dialog.Dialog.Action1() {
+ @Override
+ public void onAction(com.rey.material.dialog.Dialog dialog) {
+ SimpleDialog simpleDialog = (SimpleDialog) dialog;
+ mClickedDialogEntryIndex = simpleDialog.getSelectedIndex();
+ String value = mEntryValues[mClickedDialogEntryIndex].toString();
+ if (callChangeListener(value)) {
+ setValue(value);
+ }
+ }
+ })
+ .negativeAction(mNegativeButtonText, null);
+ final Dialog dialog = mDialog = mBuilder.build(getContext());
+ if (state != null) {
+ dialog.onRestoreInstanceState(state);
+ }
+ dialog.show();
+ }
+
+
+ public void setEntries(CharSequence[] entries) {
+ mEntries = entries;
+ }
+
+ public void setEntries(@ArrayRes int entriesResId) {
+ setEntries(getContext().getResources().getTextArray(entriesResId));
+ }
+
+ public CharSequence[] getEntries() {
+ return mEntries;
+ }
+
+ public void setEntryValues(CharSequence[] entryValues) {
+ mEntryValues = entryValues;
+ }
+
+ public void setEntryValues(@ArrayRes int entryValuesResId) {
+ setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
+ }
+
+ public CharSequence[] getEntryValues() {
+ return mEntryValues;
+ }
+
+ public void setValue(String value) {
+ final boolean changed = !TextUtils.equals(mValue, value);
+ if (changed || !mValueSet) {
+ mValue = value;
+ mValueSet = true;
+ persistString(value);
+ if (changed) {
+ notifyChanged();
+ }
+ }
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ final CharSequence entry = getEntry();
+ if (mFormat == null || entry == null) {
+ return super.getSummary();
+ } else {
+ return String.format(mFormat, entry);
+ }
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public CharSequence getEntry() {
+ int index = getValueIndex();
+ return index >= 0 && mEntries != null ? mEntries[index] : null;
+ }
+
+ public int findIndexOfValue(String value) {
+ if (value != null && mEntryValues != null) {
+ for (int i = mEntryValues.length - 1; i >= 0; i--) {
+ if (mEntryValues[i].equals(value)) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ private int getValueIndex() {
+ return findIndexOfValue(mValue);
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setValue(restoreValue ? getPersistedString(mValue) : (String) defaultValue);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ return superState;
+ }
+ final SavedState myState = new SavedState(superState);
+ myState.value = getValue();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setValue(myState.value);
+ }
+
+ private static class SavedState extends BaseSavedState {
+ String value;
+ public SavedState(Parcel source) {
+ super(source);
+ value = source.readString();
+ }
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeString(value);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/MultiSelectListPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/MultiSelectListPreference.java
new file mode 100644
index 0000000..6ffc2cc
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/MultiSelectListPreference.java
@@ -0,0 +1,228 @@
+package io.github.xhinliang.mdpreference;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.ArrayRes;
+import android.util.AttributeSet;
+
+import com.rey.material.dialog.SimpleDialog;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class MultiSelectListPreference extends DialogPreference {
+
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+ private boolean mValueSet;
+ private int selects;
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.list_preference, defStyleAttr, defStyleRes);
+ mEntries = a.getTextArray(R.styleable.list_preference_entry_arr);
+ mEntryValues = a.getTextArray(R.styleable.list_preference_value_arr);
+ a.recycle();
+ }
+
+ public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ public MultiSelectListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0, 0);
+ }
+
+ public MultiSelectListPreference(Context context) {
+ super(context);
+ init(context, null, 0, 0);
+ }
+
+
+ @Override
+ protected void onShowDialog(Bundle state) {
+ if (mEntries == null || mEntryValues == null) {
+ throw new IllegalStateException(
+ "MultiSelectListPreference requires an entries array and an entryValues array.");
+ }
+ com.rey.material.dialog.Dialog.Builder mBuilder = new SimpleDialog.Builder()
+ .multiChoiceItems(mEntries, getIndexes())
+ .title(mDialogTitle)
+ .positiveAction(mPositiveButtonText, new com.rey.material.dialog.Dialog.Action1() {
+ @Override
+ public void onAction(com.rey.material.dialog.Dialog dialog) {
+ SimpleDialog simpleDialog = (SimpleDialog) dialog;
+ int[] indexes = simpleDialog.getSelectedIndexes();
+ if (callChangeListener(indexes)) {
+ setIndexes(indexes);
+ }
+ }
+ })
+ .negativeAction(mNegativeButtonText, null);
+ final Dialog dialog = mDialog = mBuilder.build(getContext());
+ if (state != null) {
+ dialog.onRestoreInstanceState(state);
+ }
+ dialog.show();
+ }
+
+ public void setEntries(CharSequence[] entries) {
+ mEntries = entries;
+ }
+
+ public void setEntries(@ArrayRes int entriesResId) {
+ setEntries(getContext().getResources().getTextArray(entriesResId));
+ }
+
+ public CharSequence[] getEntries() {
+ return mEntries;
+ }
+
+ public void setEntryValues(CharSequence[] entryValues) {
+ mEntryValues = entryValues;
+ }
+
+ public void setEntryValues(@ArrayRes int entryValuesResId) {
+ setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
+ }
+
+ public CharSequence[] getEntryValues() {
+ return mEntryValues;
+ }
+
+ public void setSelects(int indexes) {
+ if (indexes != selects || !mValueSet) {
+ selects = indexes;
+ mValueSet = true;
+ persistInt(selects);
+ if (indexes != selects) {
+ notifyChanged();
+ }
+ }
+ }
+
+ public void setIndexes(int[] indexes) {
+ setSelects(getBit(indexes));
+ }
+
+ private int getBit(int[] indexes) {
+ int selected = 0x0;
+ for (int item : indexes) {
+ int temp = 1;
+ temp <<= item;
+ selected |= temp;
+ }
+ return selected;
+ }
+
+ private Set getArray(int bit) {
+ Set set = new HashSet<>();
+ int temp = 1;
+ for (int i = 0; i < 32; ++i) {
+ if ((temp & bit) == temp) {
+ set.add(i);
+ }
+ temp <<= 1;
+ }
+ return set;
+ }
+
+
+ public int getSelects() {
+ return selects;
+ }
+
+ public Set getSelectIndexes() {
+ return getArray(getSelects());
+ }
+
+ public int[] getIndexes() {
+ Set set = getSelectIndexes();
+ int[] result = new int[set.size()];
+ Integer[] array = new Integer[set.size()];
+ set.toArray(array);
+ for (int i = 0; i < result.length; ++i) {
+ result[i] = array[i];
+ }
+ return result;
+ }
+
+ public Set getValue() {
+ return getArray(selects);
+ }
+
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setSelects(restoreValue ? getPersistedInt(0) : 0);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ return superState;
+ }
+ final SavedState myState = new SavedState(superState);
+ super.onRestoreInstanceState(myState.getSuperState());
+ myState.value = getSelects();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setSelects(myState.value);
+ }
+
+
+ private static class SavedState extends BaseSavedState {
+ int value;
+
+ public SavedState(Parcel source) {
+ super(source);
+ value = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(value);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Preference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Preference.java
new file mode 100644
index 0000000..b515f73
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Preference.java
@@ -0,0 +1,111 @@
+package io.github.xhinliang.mdpreference;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import static android.content.Context.LAYOUT_INFLATER_SERVICE;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.text.TextUtils.isEmpty;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class Preference extends android.preference.Preference {
+
+ protected TextView titleText;
+ protected TextView summaryText;
+
+ protected ImageView imageView;
+ protected View imageFrame;
+
+ private int iconResId;
+ private Drawable icon;
+
+ public Preference(Context context) {
+ super(context);
+ init(context, null, 0, 0);
+ }
+
+ public Preference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0, 0);
+ }
+
+ public Preference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ @TargetApi(LOLLIPOP)
+ public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray typedArray = context
+ .obtainStyledAttributes(attrs, new int[]{android.R.attr.icon}, defStyleAttr, defStyleRes);
+ iconResId = typedArray.getResourceId(0, 0);
+ typedArray.recycle();
+ }
+
+ @Override
+ protected View onCreateView(ViewGroup parent) {
+ LayoutInflater layoutInflater =
+ (LayoutInflater) getContext().getSystemService(LAYOUT_INFLATER_SERVICE);
+ View layout = layoutInflater.inflate(R.layout.mp_preference, parent, false);
+
+ ViewGroup widgetFrame = (ViewGroup) layout.findViewById(R.id.widget_frame);
+ int widgetLayoutResId = getWidgetLayoutResource();
+ if (widgetLayoutResId != 0) {
+ layoutInflater.inflate(widgetLayoutResId, widgetFrame);
+ }
+ widgetFrame.setVisibility(widgetLayoutResId != 0 ? VISIBLE : GONE);
+
+ return layout;
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ CharSequence title = getTitle();
+ titleText = (TextView) view.findViewById(R.id.title);
+ titleText.setText(title);
+ titleText.setVisibility(!isEmpty(title) ? VISIBLE : GONE);
+ titleText.setTypeface(Typefaces.getRobotoRegular(getContext()));
+
+ CharSequence summary = getSummary();
+ summaryText = (TextView) view.findViewById(R.id.summary);
+ summaryText.setText(summary);
+ summaryText.setVisibility(!isEmpty(summary) ? VISIBLE : GONE);
+ summaryText.setTypeface(Typefaces.getRobotoRegular(getContext()));
+
+ if (icon == null && iconResId > 0) {
+ icon = getContext().getResources().getDrawable(iconResId);
+ }
+ imageView = (ImageView) view.findViewById(R.id.icon);
+ imageView.setImageDrawable(icon);
+ imageView.setVisibility(icon != null ? VISIBLE : GONE);
+
+ imageFrame = view.findViewById(R.id.icon_frame);
+ imageFrame.setVisibility(icon != null ? VISIBLE : GONE);
+ }
+
+ @Override
+ public void setIcon(int iconResId) {
+ super.setIcon(iconResId);
+ this.iconResId = iconResId;
+ }
+
+ @Override
+ public void setIcon(Drawable icon) {
+ super.setIcon(icon);
+ this.icon = icon;
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceCategory.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceCategory.java
new file mode 100644
index 0000000..1a609b1
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceCategory.java
@@ -0,0 +1,65 @@
+package io.github.xhinliang.mdpreference;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import static android.content.Context.LAYOUT_INFLATER_SERVICE;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.text.TextUtils.isEmpty;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class PreferenceCategory extends android.preference.PreferenceCategory {
+
+ private int accentColor;
+
+ public PreferenceCategory(Context context) {
+ super(context);
+ init();
+ }
+
+ public PreferenceCategory(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public PreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ @TargetApi(LOLLIPOP)
+ public PreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+ private void init() {
+ accentColor = ThemeUtils.resolveAccentColor(getContext());
+ }
+
+ @Override
+ protected View onCreateView(ViewGroup parent) {
+ super.onCreateView(parent);
+ LayoutInflater layoutInflater =
+ (LayoutInflater) getContext().getSystemService(LAYOUT_INFLATER_SERVICE);
+ return layoutInflater.inflate(R.layout.mp_preference_category, parent, false);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ CharSequence title = getTitle();
+ TextView titleView = (TextView) view.findViewById(R.id.title);
+ titleView.setText(title);
+ titleView.setTextColor(accentColor);
+ titleView.setVisibility(!isEmpty(title) ? VISIBLE : GONE);
+ titleView.setTypeface(Typefaces.getRobotoMedium(getContext()));
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceFragment.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceFragment.java
new file mode 100644
index 0000000..e1b41fc
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/PreferenceFragment.java
@@ -0,0 +1,20 @@
+package io.github.xhinliang.mdpreference;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ListView;
+
+public class PreferenceFragment extends android.preference.PreferenceFragment {
+
+ private static final int PADDING_LEFT_RIGHT = 0;
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ListView listView = (ListView) view.findViewById(android.R.id.list);
+ listView.setPadding(PADDING_LEFT_RIGHT, 0, PADDING_LEFT_RIGHT, 0);
+ listView.setHorizontalScrollBarEnabled(false);
+ listView.setVerticalScrollBarEnabled(false);
+ listView.setFooterDividersEnabled(false);
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/SwitchPreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/SwitchPreference.java
new file mode 100644
index 0000000..e1757da
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/SwitchPreference.java
@@ -0,0 +1,43 @@
+package io.github.xhinliang.mdpreference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Checkable;
+
+public class SwitchPreference extends TwoStatePreference {
+
+ public SwitchPreference(Context context) {
+ super(context);
+ init(context, null, 0, 0);
+ }
+
+ public SwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0, 0);
+ }
+
+ public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr, 0);
+ }
+
+ public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super.init(context, attrs, defStyleAttr, defStyleRes);
+ setWidgetLayoutResource(R.layout.mp_switch_preference);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+ Checkable checkable = (Checkable) view.findViewById(R.id.checkable);
+ checkable.setChecked(isChecked());
+ syncSummaryView();
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ThemeUtils.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ThemeUtils.java
new file mode 100644
index 0000000..016f459
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/ThemeUtils.java
@@ -0,0 +1,42 @@
+package io.github.xhinliang.mdpreference;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+
+import static android.graphics.Color.parseColor;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+
+final class ThemeUtils {
+
+ // material_deep_teal_500
+ static final int FALLBACK_COLOR = parseColor("#009688");
+
+ private ThemeUtils() {
+ // no instances
+ }
+
+ static boolean isAtLeastL() {
+ return SDK_INT >= LOLLIPOP;
+ }
+
+ @TargetApi(LOLLIPOP)
+ static int resolveAccentColor(Context context) {
+ Theme theme = context.getTheme();
+
+ // on Lollipop, grab system colorAccent attribute
+ // pre-Lollipop, grab AppCompat colorAccent attribute
+ // finally, check for custom mp_colorAccent attribute
+ int attr = isAtLeastL() ? android.R.attr.colorAccent : R.attr.colorAccent;
+ TypedArray typedArray = theme.obtainStyledAttributes(new int[]{attr, R.attr.mp_colorAccent});
+
+ int accentColor = typedArray.getColor(0, FALLBACK_COLOR);
+ accentColor = typedArray.getColor(1, accentColor);
+ typedArray.recycle();
+
+ return accentColor;
+ }
+
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/TwoStatePreference.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/TwoStatePreference.java
new file mode 100644
index 0000000..aa1137c
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/TwoStatePreference.java
@@ -0,0 +1,270 @@
+package io.github.xhinliang.mdpreference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+@SuppressWarnings("unused")
+public abstract class TwoStatePreference extends Preference {
+
+ private static final String TAG = "TwoStatePreference";
+
+ private CharSequence summaryOn;
+ private CharSequence summaryOff;
+ private boolean isChecked;
+ private boolean isCheckedSet;
+ private boolean disableDependentsState;
+
+ public TwoStatePreference(Context context) {
+ super(context);
+ }
+
+ public TwoStatePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TwoStatePreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public TwoStatePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @SuppressWarnings("ResourceType")
+ protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, new int[]{
+ android.R.attr.summaryOn, android.R.attr.summaryOff, android.R.attr.disableDependentsState
+ }, defStyleAttr, defStyleRes);
+ setSummaryOn(typedArray.getString(0));
+ setSummaryOff(typedArray.getString(1));
+ setDisableDependentsState(typedArray.getBoolean(2, false));
+ typedArray.recycle();
+ }
+
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+ boolean newValue = !isChecked();
+ if (callChangeListener(newValue)) {
+ setChecked(newValue);
+ }
+ }
+
+ /**
+ * Set the checked state and saves it to the {@link SharedPreferences}.
+ *
+ * @param checked The checked state.
+ */
+ public void setChecked(boolean checked) {
+ // Always persist/notify the first time; don't assume the field's default of false.
+ boolean changed = isChecked != checked;
+ if (changed || !isCheckedSet) {
+ isChecked = checked;
+ isCheckedSet = true;
+ persistBoolean(checked);
+ if (changed) {
+ notifyDependencyChange(shouldDisableDependents());
+ notifyChanged();
+ }
+ }
+ }
+
+
+ /**
+ * Returns the checked state.
+ *
+ * @return The checked state.
+ */
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ public boolean shouldDisableDependents() {
+ boolean shouldDisable = disableDependentsState == isChecked;
+ return shouldDisable || super.shouldDisableDependents();
+ }
+
+ /**
+ * Sets the summary to be shown when checked.
+ *
+ * @param summary The summary to be shown when checked.
+ */
+ public void setSummaryOn(CharSequence summary) {
+ summaryOn = summary;
+ if (isChecked()) {
+ notifyChanged();
+ }
+ }
+
+ /**
+ * @param summaryResId The summary as a resource.
+ * @see #setSummaryOn(CharSequence)
+ */
+ public void setSummaryOn(int summaryResId) {
+ setSummaryOn(getContext().getString(summaryResId));
+ }
+
+ /**
+ * Returns the summary to be shown when checked.
+ *
+ * @return The summary.
+ */
+ public CharSequence getSummaryOn() {
+ return summaryOn;
+ }
+
+ /**
+ * Sets the summary to be shown when unchecked.
+ *
+ * @param summary The summary to be shown when unchecked.
+ */
+ public void setSummaryOff(CharSequence summary) {
+ summaryOff = summary;
+ if (!isChecked()) {
+ notifyChanged();
+ }
+ }
+
+ /**
+ * @param summaryResId The summary as a resource.
+ * @see #setSummaryOff(CharSequence)
+ */
+ public void setSummaryOff(int summaryResId) {
+ setSummaryOff(getContext().getString(summaryResId));
+ }
+
+ /**
+ * Returns the summary to be shown when unchecked.
+ *
+ * @return The summary.
+ */
+ public CharSequence getSummaryOff() {
+ return summaryOff;
+ }
+
+ /**
+ * Returns whether dependents are disabled when this preference is on ({@code true})
+ * or when this preference is off ({@code false}).
+ *
+ * @return Whether dependents are disabled when this preference is on ({@code true})
+ * or when this preference is off ({@code false}).
+ */
+ public boolean getDisableDependentsState() {
+ return disableDependentsState;
+ }
+
+ /**
+ * Sets whether dependents are disabled when this preference is on ({@code true})
+ * or when this preference is off ({@code false}).
+ *
+ * @param disableDependentsState The preference state that should disable dependents.
+ */
+ public void setDisableDependentsState(boolean disableDependentsState) {
+ this.disableDependentsState = disableDependentsState;
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getBoolean(index, false);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setChecked(restoreValue ? getPersistedBoolean(isChecked) : (Boolean) defaultValue);
+ }
+
+ /**
+ * Sync a summary view contained within view's sub hierarchy with the correct summary text.
+ */
+ void syncSummaryView() {
+ Log.d(TAG, "syncSummaryView");
+ // Sync the summary view
+ boolean useDefaultSummary = true;
+ if (isChecked && !TextUtils.isEmpty(summaryOn)) {
+ summaryText.setText(summaryOn);
+ useDefaultSummary = false;
+ } else if (!isChecked && !TextUtils.isEmpty(summaryOff)) {
+ summaryText.setText(summaryOff);
+ useDefaultSummary = false;
+ }
+ if (useDefaultSummary) {
+ CharSequence summary = getSummary();
+ if (!TextUtils.isEmpty(summary)) {
+ summaryText.setText(summary);
+ useDefaultSummary = false;
+ }
+ }
+ int newVisibility = View.GONE;
+ if (!useDefaultSummary) {
+ // Someone has written to it
+ newVisibility = View.VISIBLE;
+ }
+ if (newVisibility != summaryText.getVisibility()) {
+ summaryText.setVisibility(newVisibility);
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+ SavedState myState = new SavedState(superState);
+ myState.checked = isChecked();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setChecked(myState.checked);
+ }
+
+ static class SavedState extends BaseSavedState {
+
+ boolean checked;
+
+ public SavedState(Parcel source) {
+ super(source);
+ checked = source.readInt() == 1;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(checked ? 1 : 0);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
+
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Typefaces.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Typefaces.java
new file mode 100644
index 0000000..1098703
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/Typefaces.java
@@ -0,0 +1,40 @@
+package io.github.xhinliang.mdpreference;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.Log;
+
+import java.util.Hashtable;
+
+public class Typefaces {
+
+ private static final String TAG = Typefaces.class.getSimpleName();
+ private static final Hashtable cache = new Hashtable<>();
+
+ private Typefaces() {
+ // no instances
+ }
+
+ static Typeface get(Context context, String assetPath) {
+ synchronized (cache) {
+ if (!cache.containsKey(assetPath)) {
+ try {
+ Typeface t = Typeface.createFromAsset(context.getAssets(), assetPath);
+ cache.put(assetPath, t);
+ } catch (Exception e) {
+ Log.e(TAG, "Could not get typeface '" + assetPath + "' Error: " + e.getMessage());
+ return null;
+ }
+ }
+ return cache.get(assetPath);
+ }
+ }
+
+ static Typeface getRobotoRegular(Context context) {
+ return get(context, "fonts/Roboto-Regular.ttf");
+ }
+
+ static Typeface getRobotoMedium(Context context) {
+ return get(context, "fonts/Roboto-Medium.ttf");
+ }
+}
diff --git a/mdpreference/src/main/java/io/github/xhinliang/mdpreference/widget/PreferenceImageView.java b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/widget/PreferenceImageView.java
new file mode 100644
index 0000000..c89285a
--- /dev/null
+++ b/mdpreference/src/main/java/io/github/xhinliang/mdpreference/widget/PreferenceImageView.java
@@ -0,0 +1,71 @@
+package io.github.xhinliang.mdpreference.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static android.view.View.MeasureSpec.getMode;
+import static android.view.View.MeasureSpec.getSize;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+import static java.lang.Integer.MAX_VALUE;
+
+/**
+ * Extension of ImageView that correctly applies maxWidth and maxHeight.
+ */
+public class PreferenceImageView extends ImageView {
+
+ private int maxWidth = MAX_VALUE >> 1;
+ private int maxHeight = MAX_VALUE >> 1;
+
+ public PreferenceImageView(Context context) {
+ super(context);
+ }
+
+ public PreferenceImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(LOLLIPOP)
+ public PreferenceImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setMaxWidth(int maxWidth) {
+ super.setMaxWidth(maxWidth);
+ this.maxWidth = maxWidth;
+ }
+
+ @Override
+ public void setMaxHeight(int maxHeight) {
+ super.setMaxHeight(maxHeight);
+ this.maxHeight = maxHeight;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = getMode(widthMeasureSpec);
+ if (widthMode == AT_MOST || widthMode == UNSPECIFIED) {
+ int widthSize = getSize(widthMeasureSpec);
+ if (maxWidth != MAX_VALUE && (maxWidth < widthSize || widthMode == UNSPECIFIED)) {
+ widthMeasureSpec = makeMeasureSpec(maxWidth, AT_MOST);
+ }
+ }
+ int heightMode = getMode(heightMeasureSpec);
+ if (heightMode == AT_MOST || heightMode == UNSPECIFIED) {
+ int heightSize = getSize(heightMeasureSpec);
+ if (maxHeight != MAX_VALUE && (maxHeight < heightSize || heightMode == UNSPECIFIED)) {
+ heightMeasureSpec = makeMeasureSpec(maxHeight, AT_MOST);
+ }
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
\ No newline at end of file
diff --git a/mdpreference/src/main/res/drawable/color_divider_light.xml b/mdpreference/src/main/res/drawable/color_divider_light.xml
new file mode 100644
index 0000000..a705c6b
--- /dev/null
+++ b/mdpreference/src/main/res/drawable/color_divider_light.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/drawable/color_label_light.xml b/mdpreference/src/main/res/drawable/color_label_light.xml
new file mode 100644
index 0000000..ee5327c
--- /dev/null
+++ b/mdpreference/src/main/res/drawable/color_label_light.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/drawable/selector.xml b/mdpreference/src/main/res/drawable/selector.xml
new file mode 100644
index 0000000..7d2917a
--- /dev/null
+++ b/mdpreference/src/main/res/drawable/selector.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/layout/mp_checkbox_preference.xml b/mdpreference/src/main/res/layout/mp_checkbox_preference.xml
new file mode 100644
index 0000000..c729396
--- /dev/null
+++ b/mdpreference/src/main/res/layout/mp_checkbox_preference.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/layout/mp_edittext.xml b/mdpreference/src/main/res/layout/mp_edittext.xml
new file mode 100644
index 0000000..db0a9bc
--- /dev/null
+++ b/mdpreference/src/main/res/layout/mp_edittext.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/layout/mp_preference.xml b/mdpreference/src/main/res/layout/mp_preference.xml
new file mode 100644
index 0000000..55a6a6b
--- /dev/null
+++ b/mdpreference/src/main/res/layout/mp_preference.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mdpreference/src/main/res/layout/mp_preference_category.xml b/mdpreference/src/main/res/layout/mp_preference_category.xml
new file mode 100644
index 0000000..c7b117b
--- /dev/null
+++ b/mdpreference/src/main/res/layout/mp_preference_category.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/layout/mp_switch_preference.xml b/mdpreference/src/main/res/layout/mp_switch_preference.xml
new file mode 100644
index 0000000..86cdf51
--- /dev/null
+++ b/mdpreference/src/main/res/layout/mp_switch_preference.xml
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/values/colors.xml b/mdpreference/src/main/res/values/colors.xml
new file mode 100644
index 0000000..5e7168a
--- /dev/null
+++ b/mdpreference/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+ #afadad
+ #009688
+
+ #FF03A9F4
+ #FF78909C
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/values/mp_attrs.xml b/mdpreference/src/main/res/values/mp_attrs.xml
new file mode 100644
index 0000000..19bc14c
--- /dev/null
+++ b/mdpreference/src/main/res/values/mp_attrs.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mdpreference/src/main/res/values/strings.xml b/mdpreference/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f43db31
--- /dev/null
+++ b/mdpreference/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ MaterialPreference
+ Cancel
+ Confirm
+
diff --git a/mdpreference/src/main/res/values/styles.xml b/mdpreference/src/main/res/values/styles.xml
new file mode 100644
index 0000000..8c92f73
--- /dev/null
+++ b/mdpreference/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..03393d0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':material', ':mdpreference'