/* * Copyright (C) 2016 Glucosio Foundation * * This file is part of Glucosio. * * Glucosio is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * Glucosio is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Glucosio. If not, see <http://www.gnu.org/licenses/>. * * */ package org.glucosio.android.tools; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.ArrayRes; import android.support.annotation.ColorRes; import android.support.annotation.LayoutRes; import android.support.annotation.StringRes; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; import org.glucosio.android.R; import java.util.List; public class LabelledSpinner extends LinearLayout implements AdapterView.OnItemSelectedListener { /** * The label positioned above the Spinner, similar to the floating * label from a {@link android.support.design.widget.TextInputLayout}. */ private TextView mLabel; /** * The Spinner component used in this layout. */ private Spinner mSpinner; /** * A thin (1dp thick) divider line positioned below the Spinner, * similar to the bottom line in an {@link android.widget.EditText}. */ private View mDivider; /** * The listener that receives notifications when an item in the * AdapterView is selected. */ private OnItemChosenListener mOnItemChosenListener; /** * The main color used in the widget (the label color and divider * color). This may be updated when XML attributes are obtained and * again if the color is set programmatically. */ private int mWidgetColor; public LabelledSpinner(Context context) { this(context, null); } public LabelledSpinner(Context context, AttributeSet attrs) { super(context, attrs); initializeViews(context, attrs); } private void initializeViews(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes( attrs, R.styleable.LabelledSpinner, 0, 0); String labelText = typedArray.getString(R.styleable.LabelledSpinner_labelText); mWidgetColor = typedArray.getColor(R.styleable.LabelledSpinner_widgetColor, getResources().getColor(R.color.widget_labelled_spinner)); typedArray.recycle(); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.widget_labelled_spinner, this, true); setOrientation(LinearLayout.VERTICAL); setLayoutParams(new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mLabel = (TextView) getChildAt(0); mLabel.setText(labelText); mLabel.setPadding(0, dpToPixels(16), 0, 0); mLabel.setTextColor(mWidgetColor); mSpinner = (Spinner) getChildAt(1); mSpinner.setPadding(0, dpToPixels(8), 0, dpToPixels(8)); mSpinner.setOnItemSelectedListener(this); mDivider = getChildAt(2); MarginLayoutParams dividerParams = (MarginLayoutParams) mDivider.getLayoutParams(); dividerParams.rightMargin = dpToPixels(4); dividerParams.bottomMargin = dpToPixels(8); mDivider.setLayoutParams(dividerParams); mDivider.setBackgroundColor(mWidgetColor); alignLabelWithSpinnerItem(4); } public TextView getLabel() { return mLabel; } public Spinner getSpinner() { return mSpinner; } public View getDivider() { return mDivider; } /** * Sets the text the label is to display. * * @param labelText The CharSequence value to be displayed on the label. * @see #setLabelText(int) */ public void setLabelText(CharSequence labelText) { mLabel.setText(labelText); } public CharSequence getLabelText() { return mLabel.getText(); } /** * Sets the text the label is to display. * * @param labelTextId The string resource identifier which refers to * the string value which is to be displayed on * the label. * @see #setLabelText(CharSequence) */ public void setLabelText(@StringRes int labelTextId) { mLabel.setText(getResources().getString(labelTextId)); } public int getColor() { return mWidgetColor; } /** * Sets the color to use for the label text and the divider line * underneath. * * @param colorRes The color resource identifier which refers to the * color that is to be displayed on the widget. */ public void setColor(@ColorRes int colorRes) { mLabel.setTextColor(getResources().getColor(colorRes)); mDivider.setBackgroundColor(getResources().getColor(colorRes)); } /** * Sets the array of items to be used in the Spinner. * * @param arrayResId The identifier of the array to use as the data * source (e.g. R.array.myArray) * @see #setItemsArray(List) * @see #setItemsArray(int, int, int) */ public void setItemsArray(@ArrayRes int arrayResId) { setItemsArray( arrayResId, android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_dropdown_item ); } /** * Sets the array of items to be used in the Spinner. * * @param list The List used as the data source * @see #setItemsArray(int) * @see #setItemsArray(int, int, int) */ public void setItemsArray(final List<?> list) { ArrayAdapter<?> adapter = new ArrayAdapter<>( getContext(), android.R.layout.simple_spinner_item, list); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); setAdapter(adapter); } private void setAdapter(ArrayAdapter<?> adapter) { //remove listener to avoid firing item selection mSpinner.setOnItemSelectedListener(null); mSpinner.setAdapter(adapter); //solution from http://stackoverflow.com/questions/2562248/how-to-keep-onitemselected-from-firing-off-on-a-newly-instantiated-spinner mSpinner.setSelection(0, false); //return back listener mSpinner.setOnItemSelectedListener(this); } /** * A private helper method to set the array of items to be used in the * Spinner. * * @param arrayResId The identifier of the array to use as the data * source (e.g. R.array.myArray) * @param spinnerItemRes The identifier of the layout used to create * views (e.g. R.layout.my_item) * @param dropdownViewRes The layout resource to create the drop down * views (e.g. R.layout.my_dropdown) * @see #setItemsArray(int) * @see #setItemsArray(List) */ private void setItemsArray(@ArrayRes int arrayResId, @LayoutRes int spinnerItemRes, @LayoutRes int dropdownViewRes) { ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource( getContext(), arrayResId, spinnerItemRes); adapter.setDropDownViewResource(dropdownViewRes); setAdapter(adapter); } /** * Sets the Adapter used to provide the data for the Spinner. * This would be similar to setting an Adapter for a normal Spinner * component. * * @param adapter The Adapter which would provide data for the Spinner */ public void setCustomAdapter(SpinnerAdapter adapter) { mSpinner.setAdapter(adapter); } /** * Sets the currently selected item. * * @param position Index (starting at 0) of the data item to be selected. */ public void setSelection(int position) { mSpinner.setSelection(position); } public OnItemChosenListener getOnItemChosenListener() { return mOnItemChosenListener; } /** * Register a callback to be invoked when an item in this AdapterView has * been selected. * This would be similar to setting an OnItemSelectedListener for a normal * Spinner component. * * @param onItemChosenListener The callback that will run */ public void setOnItemChosenListener(OnItemChosenListener onItemChosenListener) { mOnItemChosenListener = onItemChosenListener; } /** * Implemented method from {@link android.widget.AdapterView.OnItemSelectedListener} */ @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (mOnItemChosenListener != null) { // 'this' refers to this LabelledSpinner component mOnItemChosenListener.onItemChosen(this, parent, view, position, id); } } /** * Implemented method from {@link android.widget.AdapterView.OnItemSelectedListener} */ @Override public void onNothingSelected(AdapterView<?> parent) { if (mOnItemChosenListener != null) { // 'this' refers to this LabelledSpinner component mOnItemChosenListener.onNothingChosen(this, parent); } } /** * Adds a 4dp left margin to the label and divider line underneath so that * it aligns with the Spinner item text. By default, the additional 4dp * margin will not be added. * * @param indentLabel Whether or not the label will be indented * @see #alignLabelWithSpinnerItem(int) * <p/> * Note: By default, however, a 4dp margin will be added so that the label * and divider align correctly with other UI components, such as the label * in a {@link android.support.design.widget.TextInputLayout}. This means * that if {@param indentLabel} is true, an 8dp left margin will be added * (this would be the 4dp margin to align with other UI components with * an additional 4dp margin to align the label with the Spinner item text. * Also note that if {@param indentLabel} is true, the label and divider * will not be aligned with other UI components as they would be 4dp * further right from them. */ public void alignLabelWithSpinnerItem(boolean indentLabel) { if (indentLabel) { alignLabelWithSpinnerItem(8); } else { alignLabelWithSpinnerItem(4); } } /** * A helper method responsible for adding left margins to the label and * divider line underneath, used to align these to the start of the Spinner * item text. * * @param indentDps The density-independent pixel value for the left margin * @see #alignLabelWithSpinnerItem(boolean) */ private void alignLabelWithSpinnerItem(int indentDps) { MarginLayoutParams labelParams = (MarginLayoutParams) mLabel.getLayoutParams(); labelParams.leftMargin = dpToPixels(indentDps); mLabel.setLayoutParams(labelParams); MarginLayoutParams dividerParams = (MarginLayoutParams) mDivider.getLayoutParams(); dividerParams.leftMargin = dpToPixels(indentDps); mDivider.setLayoutParams(dividerParams); } /** * A helper method responsible for the conversion of dp/dip (density-independent * pixel) values to pixels, so that they can be used when setting layout * parameters such as margins. * * @param dps The density-independent pixel value * @return The pixel value from the conversion */ private int dpToPixels(int dps) { if (dps == 0) { return 0; } final float scale = getResources().getDisplayMetrics().density; return (int) (dps * scale + 0.5f); } /** * Interface definition for a callback to be invoked when an item in this * LabelledSpinner's Spinner view has been selected. */ public interface OnItemChosenListener { /** * Callback method to be invoked when an item in this LabelledSpinner's * spinner view has been selected. This callback is invoked only when * the newly selected position is different from the previously selected * position or if there was no selected item. * * @param labelledSpinner The LabelledSpinner where the selection * happened. This view contains the AdapterView. * @param adapterView The AdapterView where the selection happened. Note * that this AdapterView is part of the LabelledSpinner * component. * @param itemView The view within the AdapterView that was clicked. * @param position The position of the view in the adapter. * @param id The row id of the item that is selected. */ void onItemChosen(View labelledSpinner, AdapterView<?> adapterView, View itemView, int position, long id); /** * Callback method to be invoked when the selection disappears from this * view. The selection can disappear for instance when touch is activated * or when the adapter becomes empty. * * @param labelledSpinner The LabelledSpinner view that contains the * AdapterView. * @param adapterView The AdapterView that now contains no selected item. */ void onNothingChosen(View labelledSpinner, AdapterView<?> adapterView); } }