package com.beardedhen.androidbootstrap; 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.Bundle; import android.os.Parcelable; import android.support.v4.widget.PopupWindowCompat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.PopupWindow; import android.widget.ScrollView; import android.widget.TextView; import com.beardedhen.androidbootstrap.api.attributes.BootstrapBrand; import com.beardedhen.androidbootstrap.api.attributes.ViewGroupPosition; import com.beardedhen.androidbootstrap.api.defaults.DefaultBootstrapSize; import com.beardedhen.androidbootstrap.api.defaults.ExpandDirection; import com.beardedhen.androidbootstrap.api.view.BootstrapSizeView; import com.beardedhen.androidbootstrap.api.view.OutlineableView; import com.beardedhen.androidbootstrap.api.view.RoundableView; import com.beardedhen.androidbootstrap.utils.ColorUtils; import com.beardedhen.androidbootstrap.utils.DimenUtils; import com.beardedhen.androidbootstrap.utils.DrawableUtils; import com.beardedhen.androidbootstrap.utils.ViewUtils; import java.io.Serializable; import java.util.regex.Pattern; /** * BootstrapButtons are buttons which provide contextual menus, styled with BootstrapBrand colors, * roundable corners, and an 'outlineable' mode. */ @BetaApi public class BootstrapDropDown extends AwesomeTextView implements View.OnClickListener, RoundableView, OutlineableView, PopupWindow.OnDismissListener { private static final String TAG = "com.beardedhen.androidbootstrap.BootstrapDropDown"; private static final String KEY_DIRECTION = "com.beardedhen.androidbootstrap.BootstrapDropDown.EXPAND_DIRECTION"; private static final String SEARCH_REGEX_HEADER = "\\{dropdown_header\\}.*"; private static final String SEARCH_REGEX_SEPARATOR = "\\{dropdown_separator\\}.*"; private static final String SEARCH_REGEX_DISABLED = "\\{dropdown_disabled\\}.*"; private static final String REPLACE_REGEX_HEADER = "\\{dropdown_header\\}"; private static final String REPLACE_REGEX_SEPARATOR = "\\{dropdown_separator\\}"; private static final String REPLACE_REGEX_DISABLED = "\\{dropdown_disabled\\}"; private static final int SCREEN_WIDTH_GUESS = 1000; private ExpandDirection expandDirection; private PopupWindow dropdownWindow; private View.OnClickListener clickListener; private String[] dropdownData; private OnDropDownItemClickListener onDropDownItemClickListener; private boolean roundedCorners; private boolean showOutline; private float bootstrapSize; private int itemHeight; private int dropDownViewHeight; private int dropDownViewWidth; private int screenWidth; private float baselineStrokeWidth; private float baselineCornerRadius; private float baselineFontSize; private float baselineDropDownViewFontSize; private float baselineItemRightPadding; private float baselineItemLeftPadding; private float baselineVertPadding; private float baselineHoriPadding; public BootstrapDropDown(Context context) { super(context); initialise(null); } public BootstrapDropDown(Context context, AttributeSet attrs) { super(context, attrs); initialise(attrs); } public BootstrapDropDown(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initialise(attrs); } private void initialise(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.BootstrapDropDown); try { this.roundedCorners = a.getBoolean(R.styleable.BootstrapDropDown_roundedCorners, false); this.showOutline = a.getBoolean(R.styleable.BootstrapDropDown_showOutline, false); int directionOrdinal = a.getInt(R.styleable.BootstrapDropDown_bootstrapExpandDirection, -1); int dataOrdinal = a.getResourceId(R.styleable.BootstrapDropDown_dropdownResource, -1); int sizeOrdinal = a.getInt(R.styleable.BootstrapDropDown_bootstrapSize, -1); expandDirection = ExpandDirection.fromAttributeValue(directionOrdinal); bootstrapSize = DefaultBootstrapSize.fromAttributeValue(sizeOrdinal).scaleFactor(); itemHeight = a.getDimensionPixelSize(R.styleable.BootstrapDropDown_itemHeight, (int) DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_dropdown_default_item_height)); if (isInEditMode()) { dropdownData = new String[] {"Android Studio", "Layout Preview", "Is Always", "Breaking"}; } else { dropdownData = getContext().getResources().getStringArray(dataOrdinal); } } finally { a.recycle(); } baselineStrokeWidth = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_dropdown_default_edge_width); baselineCornerRadius = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_dropdown_default_corner_radius); baselineFontSize = DimenUtils.pixelsFromSpResource(getContext(), R.dimen.bootstrap_dropdown_default_font_size); baselineDropDownViewFontSize = DimenUtils.pixelsFromSpResource(getContext(), R.dimen.bootstrap_dropdown_default_item_font_size); baselineItemLeftPadding = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_dropdown_default_item_left_padding); baselineItemRightPadding = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_dropdown_default_item_right_padding); baselineVertPadding = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_button_default_vert_padding); baselineHoriPadding = DimenUtils.pixelsFromDpResource(getContext(), R.dimen.bootstrap_button_default_hori_padding); if (isInEditMode()) { screenWidth = SCREEN_WIDTH_GUESS; // take a sensible guess } else { DisplayMetrics metrics = new DisplayMetrics(); ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics); screenWidth = metrics.widthPixels; } createDropDown(); updateDropDownState(); } private void createDropDown() { ScrollView dropdownView = createDropDownView(); dropdownWindow = new PopupWindow(); dropdownWindow.setFocusable(true); dropdownWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); if (!isInEditMode()) { dropdownWindow.setBackgroundDrawable(DrawableUtils.resolveDrawable(android.R.drawable .dialog_holo_light_frame, getContext())); } dropdownWindow.setContentView(dropdownView); dropdownWindow.setOnDismissListener(this); dropdownWindow.setAnimationStyle(android.R.style.Animation_Activity); float longestStringWidth = measureStringWidth(getLongestString(dropdownData)) + DimenUtils.dpToPixels((baselineItemRightPadding + baselineItemLeftPadding) * bootstrapSize); if (longestStringWidth < getMeasuredWidth()) { dropdownWindow.setWidth(DimenUtils.dpToPixels(getMeasuredWidth())); } else { dropdownWindow.setWidth((int) longestStringWidth + DimenUtils.dpToPixels(8)); } } private ScrollView createDropDownView() { final LinearLayout dropdownView = new LinearLayout(getContext()); ScrollView scrollView = new ScrollView(getContext()); int clickableChildCounter = 0; dropdownView.setOrientation(LinearLayout.VERTICAL); int height = (int) (itemHeight * bootstrapSize); LayoutParams childParams = new LayoutParams(LayoutParams.MATCH_PARENT, height); for (String text : dropdownData) { TextView childView = new TextView(getContext()); childView.setGravity(Gravity.CENTER_VERTICAL); childView.setLayoutParams(childParams); int padding = (int) (baselineItemLeftPadding * bootstrapSize); childView.setPadding(padding, 0, padding, 0); childView.setTextSize(baselineDropDownViewFontSize * bootstrapSize); childView.setTextColor(ColorUtils.resolveColor(android.R.color.black, getContext())); Drawable background = getContext().obtainStyledAttributes(null, new int[]{ android.R.attr.selectableItemBackground}, 0, 0) .getDrawable(0); ViewUtils.setBackgroundDrawable(childView, background); childView.setTextColor(BootstrapDrawableFactory.bootstrapDropDownViewText(getContext())); childView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { dropdownWindow.dismiss(); if (onDropDownItemClickListener != null) { onDropDownItemClickListener.onItemClick(dropdownView, v, v.getId()); } } }); if (Pattern.matches(SEARCH_REGEX_HEADER, text)) { childView.setText(text.replaceFirst(REPLACE_REGEX_HEADER, "")); childView.setTextSize((baselineDropDownViewFontSize - 2F) * bootstrapSize); childView.setClickable(false); childView.setTextColor(ColorUtils.resolveColor(R.color.bootstrap_gray_light, getContext())); } else if (Pattern.matches(SEARCH_REGEX_SEPARATOR, text)) { childView = new DividerView(getContext()); childView.setClickable(false); childView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 3)); } else if (Pattern.matches(SEARCH_REGEX_DISABLED, text)) { childView.setEnabled(false); childView.setId(clickableChildCounter++); childView.setText(text.replaceFirst(REPLACE_REGEX_DISABLED, "")); } else { childView.setText(text); childView.setId(clickableChildCounter++); } dropdownView.addView(childView); } dropdownView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); dropDownViewHeight = dropdownView.getMeasuredHeight(); dropDownViewWidth = dropdownView.getMeasuredWidth(); scrollView.setVerticalScrollBarEnabled(false); scrollView.setHorizontalScrollBarEnabled(false); scrollView.addView(dropdownView); cleanData(); return scrollView; } private void updateDropDownState() { super.updateBootstrapState(); BootstrapBrand bootstrapBrand = getBootstrapBrand(); float cornerRadius = baselineCornerRadius; float strokeWidth = baselineStrokeWidth; final float fontSize = baselineFontSize * bootstrapSize; strokeWidth *= bootstrapSize; super.setOnClickListener(this); setTextSize(fontSize); setGravity(Gravity.CENTER); setCompoundDrawablesWithIntrinsicBounds(null, null, BootstrapDrawableFactory.bootstrapDropDownArrow(getContext(), DimenUtils.dpToPixels(8 * bootstrapSize), DimenUtils.dpToPixels(12 * bootstrapSize), expandDirection, showOutline, getBootstrapBrand()) , null); setCompoundDrawablePadding(DimenUtils.dpToPixels(8)); setTextColor(BootstrapDrawableFactory.bootstrapButtonText( getContext(), showOutline, bootstrapBrand)); Drawable bg = BootstrapDrawableFactory.bootstrapButton(getContext(), bootstrapBrand, (int) strokeWidth, (int) cornerRadius, ViewGroupPosition.SOLO, showOutline, roundedCorners); ViewUtils.setBackgroundDrawable(this, bg); int vert = (int) (baselineVertPadding * bootstrapSize); int hori = (int) (baselineHoriPadding * bootstrapSize); setPadding(hori, vert, hori, vert); } /** * Calculating string width * * @param text String to calculate * @return width of String in pixels */ private float measureStringWidth(String text) { Paint mPaint = new Paint(); mPaint.setTextSize(baselineDropDownViewFontSize * bootstrapSize); return (float) (DimenUtils.dpToPixels(mPaint.measureText(text))); } /** * Searching for longest string in array * * @param array input string array * @return longest string */ private String getLongestString(String[] array) { int maxLength = 0; String longestString = null; for (String s : array) { if (s.length() > maxLength) { maxLength = s.length(); longestString = s; } } return longestString; } private void cleanData() { String[] cleanArray = new String[dropdownData.length]; for (int i = 0; i < dropdownData.length; i++) { cleanArray[i] = dropdownData[i].replaceAll(REPLACE_REGEX_HEADER, "") .replaceAll(REPLACE_REGEX_DISABLED, "") .replaceAll(REPLACE_REGEX_SEPARATOR, ""); } dropdownData = cleanArray; } /** * Sets a listener which will be called when an item is clicked in the dropdown. * * @param onDropDownItemClickListener the listener */ public void setOnDropDownItemClickListener(OnDropDownItemClickListener onDropDownItemClickListener) { this.onDropDownItemClickListener = onDropDownItemClickListener; } @Override public boolean isShowOutline() { return showOutline; } @Override public boolean isRounded() { return roundedCorners; } /** * Gets the direction in which the dropdown expands. * * @return the direction */ public ExpandDirection getExpandDirection() { return expandDirection; } /** * Retrieves the data used to populate the dropdown. * * @return a string array of values */ public String[] getDropdownData() { return dropdownData; } @Override public void setShowOutline(boolean showOutline) { this.showOutline = showOutline; updateDropDownState(); } @Override public void setRounded(boolean rounded) { this.roundedCorners = rounded; updateDropDownState(); } /** * Sets the direction in which the dropdown should expand. * * @param expandDirection the direction */ public void setExpandDirection(ExpandDirection expandDirection) { this.expandDirection = expandDirection; updateDropDownState(); } /** * Sets the String values which should be used to populate the menu displayed in the dropdown. * * @param dropdownData an array of string values. */ public void setDropdownData(String[] dropdownData) { this.dropdownData = dropdownData; createDropDown(); updateDropDownState(); } @Override public void onDismiss() { setSelected(false); dropdownWindow.getContentView().scrollTo(0, 0); } @Override public void onClick(View v) { if (clickListener != null) { clickListener.onClick(v); } //using 8dip on axisX offset to make dropdown view visually be at start of dropdown itself //using 4dip on axisY offset to make space between dropdown view and dropdown itself //all offsets are necessary because of the dialog_holo_light_frame to display correctly on screen(shadow was made by inset) int gravity; int axisXOffset; if (dropDownViewWidth + getX() > screenWidth) { gravity = Gravity.TOP | Gravity.END; axisXOffset = DimenUtils.dpToPixels(8); } else { gravity = Gravity.TOP | Gravity.START; axisXOffset = -DimenUtils.dpToPixels(8); } int axisYOffset = DimenUtils.dpToPixels(4); switch (expandDirection) { case UP: PopupWindowCompat.showAsDropDown(dropdownWindow, v, axisXOffset, -dropDownViewHeight - getMeasuredHeight() - axisYOffset * 3, gravity); break; case DOWN: PopupWindowCompat.showAsDropDown(dropdownWindow, v, axisXOffset, -axisYOffset, gravity); break; } setSelected(true); } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable(TAG, super.onSaveInstanceState()); bundle.putBoolean(RoundableView.KEY, roundedCorners); bundle.putBoolean(OutlineableView.KEY, showOutline); bundle.putSerializable(KEY_DIRECTION, expandDirection); bundle.putFloat(BootstrapSizeView.KEY, bootstrapSize); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; this.roundedCorners = bundle.getBoolean(RoundableView.KEY); this.showOutline = bundle.getBoolean(OutlineableView.KEY); this.bootstrapSize = bundle.getFloat(BootstrapSizeView.KEY); Serializable direction = bundle.getSerializable(KEY_DIRECTION); if (direction instanceof ExpandDirection) { expandDirection = (ExpandDirection) direction; } } super.onRestoreInstanceState(state); } @Override public void setOnClickListener(OnClickListener clickListener) { this.clickListener = clickListener; } /** * A listener which provides methods relating to {@link BootstrapDropDown} */ public interface OnDropDownItemClickListener { /** * Called when an item is clicked in a {@link BootstrapDropDown} * * @param parent the parent viewgroup * @param v the view * @param id the id */ void onItemClick(ViewGroup parent, View v, int id); } private static class DividerView extends TextView { private final Paint paint; public DividerView(Context context) { super(context); paint = new Paint(); paint.setColor(ColorUtils.resolveColor(R.color.bootstrap_dropdown_divider, context)); } @Override protected void onDraw(Canvas canvas) { canvas.drawLine(0, 1, canvas.getWidth(), 1, paint); super.onDraw(canvas); } } }