package in.snoozmark.android; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Typeface; import android.os.Build; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.LinearInterpolator; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; public class MaterialSpinner extends Spinner implements ValueAnimator.AnimatorUpdateListener { public static final int DEFAULT_ARROW_WIDTH_DP = 12; private static final String TAG = MaterialSpinner.class.getSimpleName(); //Paint objects private Paint paint; private TextPaint textPaint; private StaticLayout staticLayout; private Path selectorPath; private Point[] selectorPoints; //Inner padding = "Normal" android padding private int innerPaddingLeft; private int innerPaddingRight; private int innerPaddingTop; private int innerPaddingBottom; //Private padding to add space for FloatingLabel and Underline private int extraPaddingTop; private int extraPaddingBottom; //@see dimens.xml private int underlineTopSpacing; private int underlineBottomSpacing; private int errorLabelSpacing; private int floatingLabelTopSpacing; private int floatingLabelBottomSpacing; private int floatingLabelInsideSpacing; private int rightLeftSpinnerPadding; //Properties about Error Label private int lastPosition; private ObjectAnimator errorLabelAnimator; private int errorLabelPosX; private boolean errorAnimationReverse; private int minNbErrorLines; private float currentNbErrorLines; //Properties about Floating Label ( private float floatingLabelPercent; private ObjectAnimator floatingLabelAnimator; private boolean isSelected; private boolean floatingLabelVisible; private int baseAlpha; //AttributeSet private int baseColor; private int highlightColor; private int errorColor; private int disabledColor ; private CharSequence error; private CharSequence hint; private CharSequence floatingLabelText; private int floatingLabelColor; private boolean multiline; private Typeface typeface; private boolean alignLabels; private float thickness; private float thicknessError; private int arrowColor; private float arrowSize; /* * ********************************************************************************** * CONSTRUCTORS * ********************************************************************************** */ public MaterialSpinner(Context context) { super(context); init(context, null); } public MaterialSpinner(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public MaterialSpinner(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } /* * ********************************************************************************** * INITIALISATION METHODS * ********************************************************************************** */ private void init(Context context, AttributeSet attrs) { initAttributes(context, attrs); initPaintObjects(); initDimensions(); initPadding(); initFloatingLabelAnimator(); initOnItemSelectedListener(); initAdapter(context); //Erase the drawable selector not to be affected by new size (extra paddings) setBackgroundResource(R.drawable.my_background); } private void initAttributes(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(new int[]{R.attr.colorControlNormal, R.attr.colorAccent}); int defaultBaseColor = a.getColor(0, 0); int defaultHighlightColor = a.getColor(1, 0); int defaultErrorColor = context.getResources().getColor(R.color.error_color); a.recycle(); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MaterialSpinner); baseColor = array.getColor(R.styleable.MaterialSpinner_ms_baseColor, defaultBaseColor); highlightColor = array.getColor(R.styleable.MaterialSpinner_ms_highlightColor, defaultHighlightColor); errorColor = array.getColor(R.styleable.MaterialSpinner_ms_errorColor, defaultErrorColor); disabledColor = context.getResources().getColor(R.color.disabled_color); error = array.getString(R.styleable.MaterialSpinner_ms_error); hint = array.getString(R.styleable.MaterialSpinner_ms_hint); floatingLabelText = array.getString(R.styleable.MaterialSpinner_ms_floatingLabelText); floatingLabelColor = array.getColor(R.styleable.MaterialSpinner_ms_floatingLabelColor, baseColor); multiline = array.getBoolean(R.styleable.MaterialSpinner_ms_multiline, true); minNbErrorLines = array.getInt(R.styleable.MaterialSpinner_ms_nbErrorLines, 1); alignLabels = array.getBoolean(R.styleable.MaterialSpinner_ms_alignLabels, true); thickness = array.getDimension(R.styleable.MaterialSpinner_ms_thickness, 1); thicknessError = array.getDimension(R.styleable.MaterialSpinner_ms_thickness_error, 2); arrowColor = array.getColor(R.styleable.MaterialSpinner_ms_arrowColor, baseColor); arrowSize = array.getDimension(R.styleable.MaterialSpinner_ms_arrowSize, dpToPx(DEFAULT_ARROW_WIDTH_DP)); String typefacePath = array.getString(R.styleable.MaterialSpinner_ms_typeface); if (typefacePath != null) { typeface = Typeface.createFromAsset(getContext().getAssets(), typefacePath); } array.recycle(); floatingLabelPercent = 0f; errorLabelPosX = 0; isSelected = false; floatingLabelVisible = false; lastPosition = -1; currentNbErrorLines = minNbErrorLines; } private void initPaintObjects() { int labelTextSize = getResources().getDimensionPixelSize(R.dimen.label_text_size); paint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextSize(labelTextSize); if (typeface != null) { textPaint.setTypeface(typeface); } textPaint.setColor(baseColor); baseAlpha = textPaint.getAlpha(); selectorPath = new Path(); selectorPath.setFillType(Path.FillType.EVEN_ODD); selectorPoints = new Point[3]; for (int i = 0; i < 3; i++) { selectorPoints[i] = new Point(); } } private void initPadding() { innerPaddingTop = getPaddingTop(); innerPaddingLeft = getPaddingLeft(); innerPaddingRight = getPaddingRight(); innerPaddingBottom = getPaddingBottom(); extraPaddingTop = floatingLabelTopSpacing + floatingLabelInsideSpacing + floatingLabelBottomSpacing; updateBottomPadding(); } private void updateBottomPadding() { Paint.FontMetrics textMetrics = textPaint.getFontMetrics(); extraPaddingBottom = (int) ((textMetrics.descent - textMetrics.ascent) * currentNbErrorLines) + underlineTopSpacing + underlineBottomSpacing; setPadding(); } private void initDimensions() { underlineTopSpacing = getResources().getDimensionPixelSize(R.dimen.underline_top_spacing); underlineBottomSpacing = getResources().getDimensionPixelSize(R.dimen.underline_bottom_spacing); floatingLabelTopSpacing = getResources().getDimensionPixelSize(R.dimen.floating_label_top_spacing); floatingLabelBottomSpacing = getResources().getDimensionPixelSize(R.dimen.floating_label_bottom_spacing); rightLeftSpinnerPadding = alignLabels ? getResources().getDimensionPixelSize(R.dimen.right_left_spinner_padding) : 0; floatingLabelInsideSpacing = getResources().getDimensionPixelSize(R.dimen.floating_label_inside_spacing); errorLabelSpacing = (int) getResources().getDimension(R.dimen.error_label_spacing); } private void initAdapter(final Context context) { final SpinnerAdapter adapter = new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item); setAdapter(adapter); } private void initOnItemSelectedListener() { setOnItemSelectedListener(null); } /* * ********************************************************************************** * ANIMATION METHODS * ********************************************************************************** */ private void initFloatingLabelAnimator() { if (floatingLabelAnimator == null) { floatingLabelAnimator = ObjectAnimator.ofFloat(this, "floatingLabelPercent", 0f, 1f); floatingLabelAnimator.addUpdateListener(this); } } private void showFloatingLabel() { if (floatingLabelAnimator != null) { floatingLabelVisible = true; if (floatingLabelAnimator.isRunning()) { floatingLabelAnimator.reverse(); } else { floatingLabelAnimator.start(); } } } private void hideFloatingLabel() { if (floatingLabelAnimator != null) { floatingLabelVisible = false; floatingLabelAnimator.reverse(); } } private void startErrorScrollingAnimator() { int textWidth = Math.round(textPaint.measureText(error.toString())); if (errorLabelAnimator == null) { errorLabelAnimator = ObjectAnimator.ofInt(this, "errorLabelPosX", 0, textWidth + getWidth() / 2); errorLabelAnimator.setStartDelay(1000); errorLabelAnimator.setInterpolator(new LinearInterpolator()); errorLabelAnimator.setDuration(150 * error.length()); errorLabelAnimator.addUpdateListener(this); errorLabelAnimator.setRepeatCount(ValueAnimator.INFINITE); } else { errorLabelAnimator.setIntValues(0, textWidth + getWidth() / 2); } errorLabelAnimator.start(); } private void startErrorMultilineAnimator(float destLines) { if (errorLabelAnimator == null) { errorLabelAnimator = ObjectAnimator.ofFloat(this, "currentNbErrorLines", destLines); } else { errorLabelAnimator.setFloatValues(destLines); } errorLabelAnimator.start(); } /* * ********************************************************************************** * UTILITY METHODS * ********************************************************************************** */ private int dpToPx(float dp) { DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics); return Math.round(px); } private float pxToDp(float px) { DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); return px * displayMetrics.density; } private void setPadding() { int left = innerPaddingLeft; int top = innerPaddingTop + extraPaddingTop; int right = innerPaddingRight; int bottom = innerPaddingBottom + extraPaddingBottom; super.setPadding(left, top, right, bottom); } private boolean needScrollingAnimation() { if (error != null) { float screenWidth = getWidth() - rightLeftSpinnerPadding; float errorTextWidth = textPaint.measureText(error.toString(), 0, error.length()); return errorTextWidth > screenWidth ? true : false; } return false; } private int prepareBottomPadding() { int targetNbLines = minNbErrorLines; if (error != null) { staticLayout = new StaticLayout(error, textPaint, getWidth() - getPaddingRight() - getPaddingLeft(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); int nbErrorLines = staticLayout.getLineCount(); targetNbLines = Math.max(minNbErrorLines, nbErrorLines); } return targetNbLines; } /* * ********************************************************************************** * DRAWING METHODS * ********************************************************************************** */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int startX = 0; int endX = getWidth(); int lineHeight; int startYLine = getHeight() - getPaddingBottom() + underlineTopSpacing; int startYFloatingLabel = (int) (getPaddingTop() - floatingLabelPercent * floatingLabelBottomSpacing); if (error != null) { lineHeight = dpToPx(thicknessError); int startYErrorLabel = startYLine + errorLabelSpacing + lineHeight; paint.setColor(errorColor); textPaint.setColor(errorColor); //Error Label Drawing if (multiline) { canvas.save(); canvas.translate(startX + rightLeftSpinnerPadding, startYErrorLabel - errorLabelSpacing); staticLayout.draw(canvas); canvas.restore(); } else { //scrolling canvas.drawText(error.toString(), startX + rightLeftSpinnerPadding - errorLabelPosX, startYErrorLabel, textPaint); canvas.save(); canvas.translate(textPaint.measureText(error.toString()) + getWidth() / 2, 0); canvas.drawText(error.toString(), startX + rightLeftSpinnerPadding - errorLabelPosX, startYErrorLabel, textPaint); canvas.restore(); } } else { lineHeight = dpToPx(thickness); if (isSelected) { paint.setColor(highlightColor); } else { paint.setColor(isEnabled() ? baseColor : disabledColor); } } // Underline Drawing canvas.drawRect(startX, startYLine, endX, startYLine + lineHeight, paint); //Floating Label Drawing if (hint != null || floatingLabelText != null) { if (isSelected) { textPaint.setColor(highlightColor); } else { textPaint.setColor(isEnabled() ? floatingLabelColor : disabledColor); } if (floatingLabelAnimator.isRunning() || !floatingLabelVisible) { textPaint.setAlpha((int) ((0.8 * floatingLabelPercent + 0.2) * baseAlpha * floatingLabelPercent)); } String textToDraw = floatingLabelText != null ? floatingLabelText.toString() : hint.toString(); canvas.drawText(textToDraw, startX + rightLeftSpinnerPadding, startYFloatingLabel, textPaint); } drawSelector(canvas, getWidth() - rightLeftSpinnerPadding, getPaddingTop() + dpToPx(8)); } private void drawSelector(Canvas canvas, int posX, int posY) { if (isSelected) { paint.setColor(highlightColor); } else { paint.setColor(isEnabled() ? arrowColor : disabledColor); } Point point1 = selectorPoints[0]; Point point2 = selectorPoints[1]; Point point3 = selectorPoints[2]; point1.set(posX, posY); point2.set((int) (posX - (arrowSize)), posY); point3.set((int) (posX - (arrowSize / 2)), (int) (posY + (arrowSize / 2))); selectorPath.reset(); selectorPath.moveTo(point1.x, point1.y); selectorPath.lineTo(point2.x, point2.y); selectorPath.lineTo(point3.x, point3.y); selectorPath.close(); canvas.drawPath(selectorPath, paint); } /* * ********************************************************************************** * LISTENER METHODS * ********************************************************************************** */ @Override public boolean onTouchEvent(MotionEvent event) { if (isEnabled()) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isSelected = true; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: isSelected = false; break; } invalidate(); } return super.onTouchEvent(event); } @Override public void setOnItemSelectedListener(final OnItemSelectedListener listener) { OnItemSelectedListener onItemSelectedListener = new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (hint != null || floatingLabelText != null) { if (!floatingLabelVisible && position != 0) { showFloatingLabel(); } else if (floatingLabelVisible && position == 0) { hideFloatingLabel(); } } if (position != lastPosition && error != null) { setError(null); } lastPosition = position; if (listener != null) { position = hint != null ? position - 1 : position; listener.onItemSelected(parent, view, position, id); } } @Override public void onNothingSelected(AdapterView<?> parent) { if (listener != null) { listener.onNothingSelected(parent); } } }; super.setOnItemSelectedListener(onItemSelectedListener); } @Override public void setSelection(int position, boolean animate) { boolean sameSelected = position == getSelectedItemPosition(); super.setSelection(position, animate); if (sameSelected) { // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId()); } } @Override public void setSelection(int position) { boolean sameSelected = position == getSelectedItemPosition(); super.setSelection(position); if (sameSelected) { // Spinner does not call the OnItemSelectedListener if the same item is selected, so do it manually now getOnItemSelectedListener().onItemSelected(this, getSelectedView(), position, getSelectedItemId()); } } @Override public void onAnimationUpdate(ValueAnimator animation) { invalidate(); } /* * ********************************************************************************** * GETTERS AND SETTERS * ********************************************************************************** */ public int getBaseColor() { return baseColor; } public void setBaseColor(int baseColor) { this.baseColor = baseColor; textPaint.setColor(baseColor); baseAlpha = textPaint.getAlpha(); invalidate(); } public int getHighlightColor() { return highlightColor; } public void setHighlightColor(int highlightColor) { this.highlightColor = highlightColor; invalidate(); } public int getErrorColor() { return errorColor; } public void setErrorColor(int errorColor) { this.errorColor = errorColor; invalidate(); } public void setHint(CharSequence hint) { this.hint = hint; invalidate(); } public void setHint(int resid) { CharSequence hint = getResources().getString(resid); setHint(hint); } public CharSequence getHint() { return hint; } public void setFloatingLabelText(CharSequence floatingLabelText) { this.floatingLabelText = floatingLabelText; invalidate(); } public void setFloatingLabelText(int resid) { String floatingLabelText = getResources().getString(resid); setFloatingLabelText(floatingLabelText); } public CharSequence getFloatingLabelText() { return this.floatingLabelText; } public void setError(CharSequence error) { this.error = error; if (errorLabelAnimator != null) { errorLabelAnimator.end(); } if (multiline) { startErrorMultilineAnimator(prepareBottomPadding()); } else if (needScrollingAnimation()) { startErrorScrollingAnimator(); } requestLayout(); } public void setError(int resid) { CharSequence error = getResources().getString(resid); setError(error); } @Override public void setEnabled(boolean enabled) { if(!enabled){ isSelected = false ; invalidate(); } super.setEnabled(enabled); } public CharSequence getError() { return this.error; } /** * @deprecated {use @link #setPaddingSafe(int, int, int, int)} to keep internal computation OK */ @Deprecated @Override public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); } public void setPaddingSafe(int left, int top, int right, int bottom) { innerPaddingRight = right; innerPaddingLeft = left; innerPaddingTop = top; innerPaddingBottom = bottom; setPadding(); } @Override public void setAdapter(SpinnerAdapter adapter) { super.setAdapter(new HintAdapter(adapter, getContext())); } private float getFloatingLabelPercent() { return floatingLabelPercent; } private void setFloatingLabelPercent(float floatingLabelPercent) { this.floatingLabelPercent = floatingLabelPercent; } private int getErrorLabelPosX() { return errorLabelPosX; } private void setErrorLabelPosX(int errorLabelPosX) { this.errorLabelPosX = errorLabelPosX; } private float getCurrentNbErrorLines() { return currentNbErrorLines; } private void setCurrentNbErrorLines(float currentNbErrorLines) { this.currentNbErrorLines = currentNbErrorLines; updateBottomPadding(); } /* * ********************************************************************************** * INNER CLASS * ********************************************************************************** */ private class HintAdapter extends BaseAdapter { private static final int HINT_TYPE = -1; private SpinnerAdapter mSpinnerAdapter; private Context mContext; public HintAdapter(SpinnerAdapter spinnerAdapter, Context context) { mSpinnerAdapter = spinnerAdapter; mContext = context; } @Override public int getViewTypeCount() { //Workaround waiting for a Google correction (https://code.google.com/p/android/issues/detail?id=79011) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return 1; } int viewTypeCount = mSpinnerAdapter.getViewTypeCount(); return viewTypeCount; } @Override public int getItemViewType(int position) { position = hint != null ? position - 1 : position; return (position == -1) ? HINT_TYPE : mSpinnerAdapter.getItemViewType(position); } @Override public int getCount() { int count = mSpinnerAdapter.getCount(); return hint != null ? count + 1 : count; } @Override public Object getItem(int position) { position = hint != null ? position - 1 : position; return (position == -1) ? hint : mSpinnerAdapter.getItem(position); } @Override public long getItemId(int position) { position = hint != null ? position - 1 : position; return (position == -1) ? 0 : mSpinnerAdapter.getItemId(position); } @Override public View getView(int position, View convertView, ViewGroup parent) { return buildView(position, convertView, parent, false); } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { return buildView(position, convertView, parent, true); } private View buildView(int position, View convertView, ViewGroup parent, boolean isDropDownView) { if (getItemViewType(position) == HINT_TYPE) { return getHintView(parent, isDropDownView); } //workaround to have multiple types in spinner if (convertView != null) { convertView = (convertView.getTag() != null && convertView.getTag() instanceof Integer && (Integer) convertView.getTag() != HINT_TYPE) ? convertView : null; } position = hint != null ? position - 1 : position; return isDropDownView ? mSpinnerAdapter.getDropDownView(position, convertView, parent) : mSpinnerAdapter.getView(position, convertView, parent); } private View getHintView(ViewGroup parent, boolean isDropDownView) { TextView textView; LayoutInflater inflater = LayoutInflater.from(mContext); final int resid = isDropDownView ? android.R.layout.simple_spinner_dropdown_item : android.R.layout.simple_spinner_item; textView = (TextView) inflater.inflate(resid, parent, false); textView.setText(hint); textView.setTextColor(MaterialSpinner.this.isEnabled()? baseColor : disabledColor); textView.setTag(HINT_TYPE); return textView; } } }