Android Spinner For Material Design (Android L).
Create New Project In Android Eclips.
—> AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.spinnersformaterialdesign" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="22" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="WeekView"> <attr name="wv_backgroundColor" format="reference|color"/> <attr name="wv_verticalPadding" format="reference|dimension"/> <attr name="wv_horizontalPadding" format="reference|dimension"/> <attr name="wv_animDuration" format="reference|integer"/> </declare-styleable> <declare-styleable name="ContactView"> <attr name="cv_buttonSrc" format="reference"/> <attr name="cv_buttonSize" format="reference|dimension"/> <attr name="cv_avatarSrc" format="reference"/> <attr name="cv_avatarSize" format="reference|dimension"/> <attr name="cv_spacing" format="reference|dimension"/> <attr name="cv_name" format="reference|string"/> <attr name="cv_nameTextAppearance" format="reference"/> <attr name="cv_nameTextSize" format="reference|dimension"/> <attr name="cv_nameTextColor" format="reference|color"/> <attr name="cv_address" format="reference|string"/> <attr name="cv_addressTextAppearance" format="reference"/> <attr name="cv_addressTextSize" format="reference|dimension"/> <attr name="cv_addressTextColor" format="reference|color"/> <attr name="android:minHeight" /> </declare-styleable> <declare-styleable name="ContactEditText"> <attr name="cet_spanHeight" format="reference|dimension"/> <attr name="cet_spanMaxWidth" format="reference|dimension"/> <attr name="cet_spanPaddingLeft" format="reference|dimension"/> <attr name="cet_spanPaddingRight" format="reference|dimension"/> <attr name="cet_spanFontFamily" format="reference|string"/> <attr name="cet_spanTextStyle" format="integer"> <enum name="normal" value="0" /> <enum name="bold" value="1" /> <enum name="italic" value="2" /> <enum name="bold_italic" value="3" /> </attr> <attr name="cet_spanTextSize" format="reference|dimension"/> <attr name="cet_spanTextColor" format="reference|color"/> <attr name="cet_spanBackgroundColor" format="reference|color"/> <attr name="cet_spanSpacing" format="reference|dimension"/> </declare-styleable> <attr name="pv_progressMode" format="integer"> <enum name="determinate" value="0x00000000" /> <enum name="indeterminate" value="0x00000001" /> <enum name="buffer" value="0x00000002" /> <enum name="query" value="0x00000003" /> </attr> <attr name="pv_progress" format="float"/> <attr name="pv_secondaryProgress" format="float"/> <attr name="rd_style" format="reference"/> <attr name="rd_enable" format="boolean"/> <declare-styleable name="CircularProgressDrawable"> <attr name="cpd_padding" format="reference|dimension"/> <attr name="cpd_initialAngle" format="reference|integer"/> <attr name="cpd_maxSweepAngle" format="reference|integer"/> <attr name="cpd_minSweepAngle" format="reference|integer"/> <attr name="cpd_strokeSize" format="reference|dimension"/> <attr name="cpd_strokeColor" format="reference|color"/> <attr name="cpd_strokeSecondaryColor" format="reference|color"/> <attr name="cpd_strokeColors" format="reference"/> <attr name="cpd_reverse" format="boolean"/> <attr name="cpd_rotateDuration" format="reference|integer"/> <attr name="cpd_transformDuration" format="reference|integer"/> <attr name="cpd_keepDuration" format="reference|integer"/> <attr name="cpd_transformInterpolator" format="reference"/> <attr name="cpd_inAnimDuration" format="reference|integer"/> <attr name="cpd_outAnimDuration" format="reference|integer"/> <attr name="cpd_inStepColors" format="reference"/> <attr name="cpd_inStepPercent" format="float"/> <attr name="pv_progressMode"/> <attr name="pv_progress"/> <attr name="pv_secondaryProgress"/> </declare-styleable> <declare-styleable name="LinearProgressDrawable"> <attr name="lpd_maxLineWidth" format="reference|dimension|fraction"/> <attr name="lpd_minLineWidth" format="reference|dimension|fraction"/> <attr name="lpd_strokeSize" format="reference|dimension"/> <attr name="lpd_strokeColor" format="reference|color"/> <attr name="lpd_strokeSecondaryColor" format="reference|color"/> <attr name="lpd_strokeColors" format="reference"/> <attr name="lpd_reverse" format="boolean"/> <attr name="lpd_travelDuration" format="reference|integer"/> <attr name="lpd_transformDuration" format="reference|integer"/> <attr name="lpd_keepDuration" format="reference|integer"/> <attr name="lpd_transformInterpolator" format="reference"/> <attr name="lpd_inAnimDuration" format="reference|integer"/> <attr name="lpd_outAnimDuration" format="reference|integer"/> <attr name="lpd_verticalAlign" format="integer"> <enum name="top" value="0x00000000" /> <enum name="center" value="0x00000001" /> <enum name="bottom" value="0x00000002" /> </attr> <attr name="pv_progressMode"/> <attr name="pv_progress"/> <attr name="pv_secondaryProgress"/> </declare-styleable> <declare-styleable name="ProgressView"> <attr name="pv_autostart" format="boolean"/> <attr name="pv_circular" format="boolean"/> <attr name="pv_progressStyle" format="reference"/> </declare-styleable> <declare-styleable name="RippleView"> <attr name="rd_style" /> <attr name="rd_enable" /> </declare-styleable> <declare-styleable name="RippleDrawable"> <attr name="rd_backgroundColor" format="reference|color"/> <attr name="rd_backgroundAnimDuration" format="reference|integer"/> <attr name="rd_maxRippleRadius" format="reference|dimension|integer"> <enum name="match_view" value="0x00000000" /> </attr> <attr name="rd_rippleColor" format="reference|color"/> <attr name="rd_rippleAnimDuration" format="reference|integer"/> <attr name="rd_inInterpolator" format="reference"/> <attr name="rd_outInterpolator" format="reference"/> <attr name="rd_maskType" format="integer"> <enum name="rectangle" value="0x00000000" /> <enum name="oval" value="0x00000001" /> </attr> <attr name="rd_rippleType" format="integer"> <enum name="touch" value="0x00000000" /> <enum name="wave" value="0x00000001" /> </attr> <attr name="rd_cornerRadius" format="reference|dimension"/> <attr name="rd_topLeftCornerRadius" format="reference|dimension"/> <attr name="rd_topRightCornerRadius" format="reference|dimension"/> <attr name="rd_bottomLeftCornerRadius" format="reference|dimension"/> <attr name="rd_bottomRightCornerRadius" format="reference|dimension"/> <attr name="rd_padding" format="reference|dimension"/> <attr name="rd_leftPadding" format="reference|dimension"/> <attr name="rd_topPadding" format="reference|dimension"/> <attr name="rd_rightPadding" format="reference|dimension"/> <attr name="rd_bottomPadding" format="reference|dimension"/> <attr name="rd_delayClick" format="boolean"/> </declare-styleable> <declare-styleable name="LineMorphingDrawable"> <attr name="lmd_state" format="reference"/> <attr name="lmd_curState" format="integer"/> <attr name="lmd_padding" format="reference|dimension"/> <attr name="lmd_paddingLeft" format="reference|dimension"/> <attr name="lmd_paddingTop" format="reference|dimension"/> <attr name="lmd_paddingRight" format="reference|dimension"/> <attr name="lmd_paddingBottom" format="reference|dimension"/> <attr name="lmd_animDuration" format="reference|integer"/> <attr name="lmd_interpolator" format="reference"/> <attr name="lmd_strokeSize" format="reference|dimension"/> <attr name="lmd_strokeColor" format="reference|color"/> <attr name="lmd_strokeCap" format="integer"> <enum name="butt" value="0x00000000" /> <enum name="round" value="0x00000001" /> <enum name="square" value="0x00000002" /> </attr> <attr name="lmd_strokeJoin" format="integer"> <enum name="miter" value="0x00000000" /> <enum name="round" value="0x00000001" /> <enum name="bevel" value="0x00000002" /> </attr> <attr name="lmd_clockwise" format="boolean"/> </declare-styleable> <declare-styleable name="RadioButtonDrawable"> <attr name="rbd_width" format="reference|dimension"/> <attr name="rbd_height" format="reference|dimension"/> <attr name="rbd_strokeSize" format="reference|dimension"/> <attr name="rbd_radius" format="reference|dimension"/> <attr name="rbd_innerRadius" format="reference|dimension"/> <attr name="rbd_strokeColor" format="reference|color"/> <attr name="rbd_animDuration" format="reference|integer"/> </declare-styleable> <declare-styleable name="CheckBoxDrawable"> <attr name="cbd_width" format="reference|dimension"/> <attr name="cbd_height" format="reference|dimension"/> <attr name="cbd_boxSize" format="reference|dimension"/> <attr name="cbd_cornerRadius" format="reference|dimension"/> <attr name="cbd_strokeSize" format="reference|dimension"/> <attr name="cbd_strokeColor" format="reference|color"/> <attr name="cbd_tickColor" format="reference|color"/> <attr name="cbd_animDuration" format="reference|integer"/> </declare-styleable> <declare-styleable name="Switch"> <attr name="sw_trackSize" format="reference|dimension"/> <attr name="sw_trackColor" format="reference|color"/> <attr name="sw_trackCap" format="integer"> <enum name="butt" value="0x00000000" /> <enum name="round" value="0x00000001" /> <enum name="square" value="0x00000002" /> </attr> <attr name="sw_thumbColor" format="reference|color"/> <attr name="sw_thumbRadius" format="reference|dimension"/> <attr name="sw_thumbElevation" format="reference|dimension"/> <attr name="sw_animDuration" format="reference|integer"/> <attr name="sw_interpolator" format="reference"/> <attr name="android:gravity" /> <attr name="android:checked" /> </declare-styleable> <declare-styleable name="Slider"> <attr name="sl_trackSize" format="reference|dimension"/> <attr name="sl_primaryColor" format="reference|color"/> <attr name="sl_secondaryColor" format="reference|color"/> <attr name="sl_trackCap" format="integer"> <enum name="butt" value="0x00000000" /> <enum name="round" value="0x00000001" /> <enum name="square" value="0x00000002" /> </attr> <attr name="sl_thumbBorderSize" format="reference|dimension"/> <attr name="sl_thumbRadius" format="reference|dimension"/> <attr name="sl_thumbFocusRadius" format="reference|dimension"/> <attr name="sl_travelAnimDuration" format="reference|integer"/> <attr name="sl_transformAnimDuration" format="reference|integer"/> <attr name="sl_interpolator" format="reference"/> <attr name="sl_minValue" format="reference|integer"/> <attr name="sl_maxValue" format="reference|integer"/> <attr name="sl_stepValue" format="reference|integer"/> <attr name="sl_value" format="reference|integer"/> <attr name="sl_discreteMode" format="reference|boolean"/> <attr name="sl_fontFamily" format="string|reference"/> <attr name="sl_textStyle" format="integer"> <enum name="normal" value="0" /> <enum name="bold" value="1" /> <enum name="italic" value="2" /> <enum name="bold_italic" value="3" /> </attr> <attr name="sl_textSize" format="reference|dimension"/> <attr name="sl_textColor" format="reference|color"/> <attr name="android:gravity" /> <attr name="android:enabled" /> </declare-styleable> <declare-styleable name="NavigationDrawerDrawable"> <attr name="nd_ripple" format="reference"/> <attr name="nd_icon" format="reference"/> </declare-styleable> <declare-styleable name="TabPageIndicator"> <attr name="tpi_tabPadding" format="reference|dimension"/> <attr name="tpi_tabRipple" format="reference"/> <attr name="tpi_indicatorColor" format="reference|color"/> <attr name="tpi_indicatorHeight" format="reference|dimension"/> <attr name="android:textAppearance" /> <attr name="tpi_mode" format="integer"> <enum name="scroll" value="0x00000000" /> <enum name="fixed" value="0x00000001" /> </attr> </declare-styleable> <declare-styleable name="EditText"> <attr name="et_inputId" format="reference"/> <attr name="et_labelEnable" format="boolean"/> <attr name="et_labelPadding" format="reference|dimension"/> <attr name="et_labelTextSize" format="reference|dimension"/> <attr name="et_labelTextColor" format="reference|color"/> <attr name="et_labelTextAppearance" format="reference"/> <attr name="et_labelEllipsize" format="integer"> <enum name="start" value="0x00000001" /> <enum name="middle" value="0x00000002" /> <enum name="end" value="0x00000003" /> <enum name="marquee" value="0x00000004" /> </attr> <attr name="et_labelInAnim" format="reference"/> <attr name="et_labelOutAnim" format="reference"/> <attr name="et_supportMode" format="integer"> <enum name="none" value="0x00000000" /> <enum name="helper" value="0x00000001" /> <enum name="helperWithError" value="0x00000002" /> <enum name="charCounter" value="0x00000003" /> </attr> <attr name="et_supportMaxChars" format="reference|integer"/> <attr name="et_helper" format="reference|string"/> <attr name="et_error" format="reference|string"/> <attr name="et_supportPadding" format="reference|dimension"/> <attr name="et_supportTextSize" format="reference|dimension"/> <attr name="et_supportTextColor" format="reference|color"/> <attr name="et_supportTextErrorColor" format="reference|color"/> <attr name="et_supportTextAppearance" format="reference"/> <attr name="et_supportSingleLine" format="boolean"/> <attr name="et_supportMaxLines" format="reference|integer"/> <attr name="et_supportLines" format="reference|integer"/> <attr name="et_supportEllipsize" format="integer"> <enum name="start" value="0x00000001" /> <enum name="middle" value="0x00000002" /> <enum name="end" value="0x00000003" /> <enum name="marquee" value="0x00000004" /> </attr> <attr name="et_dividerColor" format="reference|color"/> <attr name="et_dividerErrorColor" format="reference|color"/> <attr name="et_dividerHeight" format="reference|dimension"/> <attr name="et_dividerPadding" format="reference|dimension"/> <attr name="et_dividerAnimDuration" format="reference|integer"/> <attr name="et_dividerCompoundPadding" format="boolean"/> <attr name="et_autoCompleteMode" format="integer"> <enum name="none" value="0"/> <enum name="single" value="1"/> <enum name="multi" value="2"/> </attr> </declare-styleable> <declare-styleable name="SnackBar"> <attr name="sb_backgroundColor" format="reference|color"/> <attr name="sb_backgroundCornerRadius" format="reference|dimension"/> <attr name="sb_horizontalPadding" format="reference|dimension"/> <attr name="sb_verticalPadding" format="reference|dimension"/> <attr name="sb_width" format="reference|dimension|integer"> <enum name="match_parent" value="-1" /> <enum name="wrap_content" value="-2" /> </attr> <attr name="sb_minWidth" format="reference|dimension"/> <attr name="sb_maxWidth" format="reference|dimension"/> <attr name="sb_height" format="reference|dimension|integer"> <enum name="match_parent" value="-1" /> <enum name="wrap_content" value="-2" /> </attr> <attr name="sb_minHeight" format="reference|dimension"/> <attr name="sb_maxHeight" format="reference|dimension"/> <attr name="sb_marginLeft" format="reference|dimension"/> <attr name="sb_marginBottom" format="reference|dimension"/> <attr name="sb_textSize" format="reference|dimension"/> <attr name="sb_textColor" format="reference|color"/> <attr name="sb_textAppearance" format="reference"/> <attr name="sb_text" format="reference|string"/> <attr name="sb_singleLine" format="boolean"/> <attr name="sb_maxLines" format="reference|integer"/> <attr name="sb_lines" format="reference|integer"/> <attr name="sb_ellipsize" format="integer"> <enum name="start" value="0x00000001" /> <enum name="middle" value="0x00000002" /> <enum name="end" value="0x00000003" /> <enum name="marquee" value="0x00000004" /> </attr> <attr name="sb_actionTextSize" format="reference|dimension"/> <attr name="sb_actionTextColor" format="reference|color"/> <attr name="sb_actionTextAppearance" format="reference"/> <attr name="sb_actionText" format="reference|string"/> <attr name="sb_actionRipple" format="reference"/> <attr name="sb_duration" format="reference|integer"/> <attr name="sb_inAnimation" format="reference"/> <attr name="sb_outAnimation" format="reference"/> <attr name="sb_removeOnDismiss" format="boolean" /> </declare-styleable> <declare-styleable name="FloatingActionButton"> <attr name="fab_backgroundColor" format="reference|color"/> <attr name="fab_radius" format="reference|dimension"/> <attr name="fab_elevation" format="reference|dimension"/> <attr name="fab_iconSrc" format="reference"/> <attr name="fab_iconLineMorphing" format="reference"/> <attr name="fab_iconSize" format="reference|dimension"/> <attr name="fab_interpolator" format="reference"/> <attr name="fab_animDuration" format="reference|integer"/> </declare-styleable> <declare-styleable name="Spinner"> <attr name="android:dropDownWidth" /> <attr name="android:popupBackground" /> <attr name="android:minWidth"/> <attr name="android:minHeight"/> <attr name="spn_labelEnable" format="boolean"/> <attr name="spn_labelPadding" format="reference|dimension"/> <attr name="spn_labelTextSize" format="reference|dimension"/> <attr name="spn_labelTextColor" format="reference|color"/> <attr name="spn_labelTextAppearance" format="reference"/> <attr name="spn_labelEllipsize" format="integer"> <enum name="start" value="0x00000001" /> <enum name="middle" value="0x00000002" /> <enum name="end" value="0x00000003" /> <enum name="marquee" value="0x00000004" /> </attr> <attr name="spn_label" format="string|reference"/> <attr name="spn_popupItemAnimation" format="reference"/> <attr name="spn_popupItemAnimOffset" format="integer"/> <attr name="spn_arrowColor" format="reference|color"/> <attr name="spn_arrowSize" format="reference|dimension"/> <attr name="spn_arrowPadding" format="reference|dimension"/> <attr name="spn_arrowAnimDuration" format="integer"/> <attr name="spn_arrowInterpolator" format="reference"/> <attr name="spn_arrowAnimClockwise" format="boolean"/> <attr name="spn_arrowSwitchMode" format="boolean"/> <attr name="spn_dividerColor" format="reference|color"/> <attr name="spn_dividerHeight" format="reference|dimension"/> <attr name="spn_dividerPadding" format="reference|dimension"/> <attr name="spn_dividerAnimDuration" format="reference|integer"/> </declare-styleable> <declare-styleable name="Dialog"> <attr name="android:layout_width" /> <attr name="android:layout_height" /> <attr name="di_dimAmount" format="float" /> <attr name="di_backgroundColor" format="color|reference" /> <attr name="di_elevation" format="dimension|reference" /> <attr name="di_maxElevation" format="dimension|reference" /> <attr name="di_cornerRadius" format="dimension|reference" /> <attr name="di_titleTextAppearance" format="reference" /> <attr name="di_titleTextColor" format="color|reference" /> <attr name="di_actionBackground" format="reference" /> <attr name="di_actionRipple" format="reference" /> <attr name="di_actionTextAppearance" format="reference" /> <attr name="di_actionTextColor" format="color|reference" /> <attr name="di_positiveActionBackground" format="reference" /> <attr name="di_positiveActionRipple" format="reference" /> <attr name="di_positiveActionTextAppearance" format="reference" /> <attr name="di_positiveActionTextColor" format="color|reference" /> <attr name="di_negativeActionBackground" format="reference" /> <attr name="di_negativeActionRipple" format="reference" /> <attr name="di_negativeActionTextAppearance" format="reference" /> <attr name="di_negativeActionTextColor" format="color|reference" /> <attr name="di_neutralActionBackground" format="reference" /> <attr name="di_neutralActionRipple" format="reference" /> <attr name="di_neutralActionTextAppearance" format="reference" /> <attr name="di_neutralActionTextColor" format="color|reference" /> <attr name="di_dividerColor" format="color|reference" /> <attr name="di_dividerHeight" format="dimension|reference" /> <attr name="di_inAnimation" format="reference" /> <attr name="di_outAnimation" format="reference" /> <attr name="di_cancelable" format="boolean" /> <attr name="di_canceledOnTouchOutside" format="boolean" /> </declare-styleable> <declare-styleable name="SimpleDialog"> <attr name="di_messageTextAppearance" format="reference" /> <attr name="di_messageTextColor" format="color|reference" /> <attr name="di_radioButtonStyle" format="reference" /> <attr name="di_checkBoxStyle" format="reference" /> <attr name="di_itemHeight" format="dimension|reference" /> <attr name="di_itemTextAppearance" format="reference" /> </declare-styleable> <declare-styleable name="TimePicker"> <attr name="tp_backgroundColor" format="color|reference" /> <attr name="tp_selectionColor" format="color|reference" /> <attr name="tp_selectionRadius" format="dimension|reference" /> <attr name="tp_tickSize" format="dimension|reference" /> <attr name="tp_fontFamily" format="string|reference"/> <attr name="tp_textStyle" format="integer"> <enum name="normal" value="0" /> <enum name="bold" value="1" /> <enum name="italic" value="2" /> <enum name="bold_italic" value="3" /> </attr> <attr name="tp_textSize" format="dimension|reference" /> <attr name="tp_textColor" format="color|reference" /> <attr name="tp_textHighlightColor" format="color|reference" /> <attr name="tp_animDuration" format="integer|reference" /> <attr name="tp_inInterpolator" format="reference" /> <attr name="tp_outInterpolator" format="reference" /> <attr name="tp_mode" format="integer"> <enum name="hour" value="0" /> <enum name="minute" value="1" /> </attr> <attr name="tp_24Hour" format="boolean"/> <attr name="tp_hour" format="integer"/> <attr name="tp_minute" format="integer"/> </declare-styleable> <declare-styleable name="TimePickerDialog"> <attr name="tp_headerHeight" format="dimension|reference" /> <attr name="tp_textTimeColor" format="color|reference" /> <attr name="tp_textTimeSize" format="dimension|reference" /> <attr name="tp_am" format="string|reference"/> <attr name="tp_pm" format="string|reference"/> </declare-styleable> <attr name="dp_textHighlightColor" format="color|reference" /> <attr name="dp_textColor" format="color|reference" /> <attr name="dp_selectionColor" format="color|reference" /> <attr name="dp_animDuration" format="integer|reference" /> <attr name="dp_inInterpolator" format="reference" /> <attr name="dp_outInterpolator" format="reference" /> <attr name="dp_yearMin" format="integer"/> <attr name="dp_monthMin" format="integer"/> <attr name="dp_dayMin" format="integer"/> <attr name="dp_yearMax" format="integer"/> <attr name="dp_monthMax" format="integer"/> <attr name="dp_dayMax" format="integer"/> <attr name="dp_year" format="integer"/> <attr name="dp_month" format="integer"/> <attr name="dp_day" format="integer"/> <attr name="dp_fontFamily" format="string|reference"/> <attr name="dp_textStyle" format="integer"> <enum name="normal" value="0" /> <enum name="bold" value="1" /> <enum name="italic" value="2" /> <enum name="bold_italic" value="3" /> </attr> <declare-styleable name="YearPicker"> <attr name="dp_yearMin" /> <attr name="dp_yearMax" /> <attr name="dp_year" /> <attr name="dp_yearTextSize" format="dimension|reference" /> <attr name="dp_yearItemHeight" format="dimension|reference" /> <attr name="dp_textHighlightColor" /> <attr name="dp_textColor" /> <attr name="dp_selectionColor" /> <attr name="dp_animDuration" /> <attr name="dp_inInterpolator" /> <attr name="dp_outInterpolator" /> <attr name="dp_fontFamily" /> <attr name="dp_textStyle" /> </declare-styleable> <declare-styleable name="DatePicker"> <attr name="dp_yearMin" /> <attr name="dp_monthMin" /> <attr name="dp_dayMin" /> <attr name="dp_yearMax" /> <attr name="dp_monthMax" /> <attr name="dp_dayMax" /> <attr name="dp_year" /> <attr name="dp_month" /> <attr name="dp_day" /> <attr name="dp_dayTextSize" format="dimension|reference" /> <attr name="dp_textLabelColor" format="color|reference" /> <attr name="dp_textDisableColor" format="color|reference" /> <attr name="dp_textHighlightColor" /> <attr name="dp_textColor" /> <attr name="dp_selectionColor" /> <attr name="dp_animDuration" /> <attr name="dp_inInterpolator" /> <attr name="dp_outInterpolator" /> <attr name="dp_fontFamily" /> <attr name="dp_textStyle" /> <attr name="android:padding" /> <attr name="android:paddingLeft" /> <attr name="android:paddingTop" /> <attr name="android:paddingRight" /> <attr name="android:paddingBottom" /> </declare-styleable> <declare-styleable name="DatePickerDialog"> <attr name="dp_headerPrimaryHeight" format="dimension|reference" /> <attr name="dp_headerPrimaryColor" format="color|reference" /> <attr name="dp_headerSecondaryHeight" format="dimension|reference" /> <attr name="dp_headerSecondaryColor" format="color|reference" /> <attr name="dp_headerPrimaryTextSize" format="dimension|reference" /> <attr name="dp_headerSecondaryTextSize" format="dimension|reference" /> <attr name="dp_textHeaderColor" format="color|reference" /> </declare-styleable> </resources>
styles.xml
<resources> <!-- Base application theme, dependent on API level. This theme is replaced by AppBaseTheme from res/values-vXX/styles.xml on newer devices. --> <style name="AppBaseTheme" parent="Theme.AppCompat.Light"> <!-- Theme customizations available in newer API levels can go in res/values-vXX/styles.xml, while customizations related to backward-compatibility can go here. --> </style> <!-- Application theme. --> <style name="AppTheme" parent="AppBaseTheme"> <!-- All customizations that are NOT specific to a particular API-level can go here. --> </style> <style name="MenuItemRippleStyle" parent="Material.Drawable.Ripple.Wave"> <item name="rd_rippleColor">#902DA0FF</item> <item name="rd_rippleAnimDuration">300</item> <item name="rd_maskType">rectangle</item> <item name="rd_cornerRadius">0dp</item> </style> <!-- AppBar Style --> <style name="AppBar" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <item name="selectableItemBackground">@null</item> </style> <!-- ContactView --> <!-- <style name="ContactView"> <item name="android:minHeight">56dp</item> <item name="cv_buttonSize">0dp</item> <item name="cv_avatarSize">40dp</item> <item name="cv_spacing">8dp</item> <item name="cv_nameTextAppearance">@style/TextAppearance.AppCompat.Subhead</item> <item name="cv_nameTextColor">@color/abc_primary_text_material_light</item> <item name="cv_addressTextAppearance">@style/TextAppearance.AppCompat.Body1</item> <item name="cv_addressTextColor">@color/abc_primary_text_material_light</item> </style> <style name="ReplacementContactView" parent="Material.Drawable.Ripple.Wave.Light"> <item name="rd_cornerRadius">0dp</item> <item name="rd_delayClick">false</item> <item name="android:minHeight">56dp</item> <item name="cv_buttonSize">0dp</item> <item name="cv_avatarSize">40dp</item> <item name="cv_spacing">8dp</item> <item name="cv_nameTextAppearance">@style/TextAppearance.AppCompat.Subhead</item> <item name="cv_nameTextColor">@color/abc_primary_text_material_light</item> <item name="cv_addressTextAppearance">@style/TextAppearance.AppCompat.Body1</item> <item name="cv_addressTextColor">@color/abc_primary_text_material_light</item> </style> <style name="SelectedContactView" parent="Material.Drawable.Ripple.Wave.Light"> <item name="android:background">#FF00B8D4</item> <item name="rd_cornerRadius">0dp</item> <item name="rd_delayClick">false</item> <item name="android:minHeight">72dp</item> <item name="cv_buttonSrc">@drawable/ic_cancel_white_24dp</item> <item name="cv_buttonSize">48dp</item> <item name="cv_nameTextColor">#FFFFFFFF</item> <item name="cv_addressTextColor">#FFFFFFFF</item> <item name="cv_avatarSize">40dp</item> <item name="cv_spacing">8dp</item> <item name="cv_nameTextAppearance">@style/TextAppearance.AppCompat.Subhead</item> <item name="cv_addressTextAppearance">@style/TextAppearance.AppCompat.Body1</item> </style> <style name="ContactEditText" parent="Material.Widget.EditText.Light"> <item name="cet_spanHeight">32dp</item> <item name="cet_spanMaxWidth">150dp</item> <item name="cet_spanPaddingLeft">8dp</item> <item name="cet_spanPaddingRight">12dp</item> <item name="cet_spanTextSize">14sp</item> <item name="cet_spanTextColor">@color/abc_primary_text_material_light</item> <item name="cet_spanBackgroundColor">#FFE0E0E0</item> <item name="cet_spanSpacing">4dp</item> </style> --> <style name="Material"></style> <style name="Material.Drawable"></style> <style name="Material.Widget"></style> <style name="Material.App"></style> <style name="Material.TextAppearance"></style> <style name="Material.Drawable.CircularProgress"> <item name="cpd_padding">0dp</item> <item name="cpd_initialAngle">0</item> <item name="cpd_maxSweepAngle">270</item> <item name="cpd_minSweepAngle">1</item> <item name="cpd_strokeSize">4dp</item> <item name="cpd_strokeColor">?attr/colorPrimary</item> <item name="cpd_strokeSecondaryColor">@android:color/transparent</item> <item name="cpd_reverse">false</item> <item name="cpd_rotateDuration">1000</item> <item name="cpd_transformDuration">600</item> <item name="cpd_keepDuration">200</item> <item name="cpd_transformInterpolator">@android:anim/decelerate_interpolator</item> <item name="pv_progressMode">indeterminate</item> <item name="cpd_inAnimDuration">0</item> <item name="cpd_outAnimDuration">@android:integer/config_mediumAnimTime</item> </style> <style name="Material.Drawable.CircularProgress.Determinate" parent="Material.Drawable.CircularProgress"> <item name="pv_progressMode">determinate</item> <item name="cpd_inAnimDuration">@android:integer/config_mediumAnimTime</item> </style> <style name="Material.Drawable.LinearProgress"> <item name="lpd_maxLineWidth">75%</item> <item name="lpd_minLineWidth">10%</item> <item name="lpd_strokeSize">4dp</item> <item name="lpd_strokeColor">?attr/colorPrimary</item> <item name="lpd_strokeSecondaryColor">?attr/colorControlNormal</item> <item name="lpd_reverse">false</item> <item name="lpd_travelDuration">1000</item> <item name="lpd_transformDuration">600</item> <item name="lpd_keepDuration">200</item> <item name="lpd_transformInterpolator">@android:anim/decelerate_interpolator</item> <item name="pv_progressMode">indeterminate</item> <item name="lpd_inAnimDuration">@android:integer/config_mediumAnimTime</item> <item name="lpd_outAnimDuration">@android:integer/config_mediumAnimTime</item> <item name="lpd_verticalAlign">bottom</item> </style> <style name="Material.Drawable.LinearProgress.Determinate" parent="Material.Drawable.LinearProgress"> <item name="pv_progressMode">determinate</item> </style> <style name="Material.Drawable.LinearProgress.Query" parent="Material.Drawable.LinearProgress"> <item name="pv_progressMode">query</item> </style> <style name="Material.Drawable.LinearProgress.Buffer" parent="Material.Drawable.LinearProgress"> <item name="pv_progressMode">buffer</item> </style> <style name="Material.Widget.ProgressView"> <item name="pv_progress">0</item> </style> <style name="Material.Widget.ProgressView.Circular" parent="Material.Widget.ProgressView"> <item name="pv_autostart">true</item> <item name="pv_circular">true</item> <item name="pv_progressStyle">@style/Material.Drawable.CircularProgress</item> <item name="pv_progressMode">indeterminate</item> </style> <style name="Material.Widget.ProgressView.Circular.Determinate" parent="Material.Widget.ProgressView.Circular"> <item name="pv_progressStyle">@style/Material.Drawable.CircularProgress.Determinate</item> <item name="pv_progressMode">determinate</item> </style> <style name="Material.Widget.ProgressView.Linear" parent="Material.Widget.ProgressView"> <item name="pv_autostart">true</item> <item name="pv_circular">false</item> <item name="pv_progressStyle">@style/Material.Drawable.LinearProgress</item> <item name="pv_progressMode">indeterminate</item> </style> <style name="Material.Widget.ProgressView.Linear.Determinate" parent="Material.Widget.ProgressView.Linear"> <item name="pv_progressStyle">@style/Material.Drawable.LinearProgress.Determinate</item> <item name="pv_progressMode">determinate</item> </style> <style name="Material.Widget.ProgressView.Linear.Query" parent="Material.Widget.ProgressView.Linear"> <item name="pv_progressStyle">@style/Material.Drawable.LinearProgress.Query</item> <item name="pv_progressMode">query</item> </style> <style name="Material.Widget.ProgressView.Linear.Buffer" parent="Material.Widget.ProgressView.Linear"> <item name="pv_progressStyle">@style/Material.Drawable.LinearProgress.Buffer</item> <item name="pv_progressMode">buffer</item> </style> <style name="Material.Drawable.Ripple"> <item name="rd_enable">true</item> <item name="rd_inInterpolator">@android:anim/decelerate_interpolator</item> <item name="rd_outInterpolator">@android:anim/decelerate_interpolator</item> <item name="rd_maskType">rectangle</item> <item name="rd_cornerRadius">2dp</item> <item name="rd_padding">0dp</item> <item name="rd_delayClick">false</item> </style> <style name="Material.Drawable.Ripple.Touch" parent="Material.Drawable.Ripple"> <item name="rd_backgroundColor">#26CCCCCC</item> <item name="rd_backgroundAnimDuration">200</item> <item name="rd_maxRippleRadius">48dp</item> <item name="rd_rippleColor">#19CCCCCC</item> <item name="rd_rippleAnimDuration">300</item> <item name="rd_rippleType">touch</item> </style> <style name="Material.Drawable.Ripple.Touch.MatchView" parent="Material.Drawable.Ripple.Touch"> <item name="rd_maxRippleRadius">match_view</item> </style> <style name="Material.Drawable.Ripple.Touch.Light" parent="Material.Drawable.Ripple.Touch"> <item name="rd_backgroundColor">#33999999</item> <item name="rd_rippleColor">#33999999</item> </style> <style name="Material.Drawable.Ripple.Touch.MatchView.Light" parent="Material.Drawable.Ripple.Touch"> <item name="rd_maxRippleRadius">match_view</item> <item name="rd_backgroundColor">#33999999</item> <item name="rd_rippleColor">#33999999</item> </style> <style name="Material.Drawable.Ripple.Wave" parent="Material.Drawable.Ripple"> <item name="rd_maxRippleRadius">48dp</item> <item name="rd_rippleColor">#3FCCCCCC</item> <item name="rd_rippleAnimDuration">200</item> <item name="rd_rippleType">wave</item> </style> <style name="Material.Drawable.Ripple.Wave.Light" parent="Material.Drawable.Ripple.Wave"> <item name="rd_rippleColor">#66999999</item> </style> <style name="Material.Widget.Slider"> <item name="sl_trackSize">2dp</item> <item name="sl_trackCap">square</item> <item name="sl_thumbBorderSize">2dp</item> <item name="sl_thumbRadius">10dp</item> <item name="sl_thumbFocusRadius">14dp</item> <item name="sl_travelAnimDuration">@android:integer/config_mediumAnimTime</item> <item name="sl_transformAnimDuration">@android:integer/config_shortAnimTime</item> <item name="sl_interpolator">@android:anim/decelerate_interpolator</item> <item name="sl_minValue">0</item> <item name="sl_maxValue">100</item> <item name="sl_stepValue">1</item> <item name="sl_value">0</item> <item name="sl_discreteMode">false</item> <item name="sl_textSize">@dimen/abc_text_size_small_material</item> <item name="sl_textColor">#FFFFFFFF</item> </style> <style name="Material.Widget.Slider.Discrete" parent="Material.Widget.Slider"> <item name="sl_discreteMode">true</item> </style> <style name="Material.Widget.TabPageIndicator"> <item name="tpi_tabPadding">12dp</item> <item name="tpi_tabRipple">@style/Material.Drawable.Ripple.Wave</item> <item name="tpi_indicatorHeight">2dp</item> <item name="android:textAppearance">?attr/textAppearanceListItem</item> <item name="tpi_mode">scroll</item> </style> <style name="Material.Widget.TabPageIndicator.Light" parent="Material.Widget.TabPageIndicator"> <item name="tpi_tabRipple">@style/Material.Drawable.Ripple.Wave.Light</item> </style> <style name="Material.Widget.TabPageIndicator.Fixed" parent="Material.Widget.TabPageIndicator"> <item name="tpi_mode">fixed</item> </style> <style name="Material.Widget.TabPageIndicator.Fixed.Light" parent="Material.Widget.TabPageIndicator.Light"> <item name="tpi_mode">fixed</item> </style> <style name="Material.Widget.FloatingActionButton" parent="Material.Drawable.Ripple.Touch"> <item name="fab_radius">28dp</item> <item name="fab_elevation">4dp</item> <item name="fab_iconSize">24dp</item> <item name="fab_interpolator">@android:anim/decelerate_interpolator</item> <item name="fab_animDuration">@android:integer/config_mediumAnimTime</item> </style> <style name="Material.Widget.FloatingActionButton.Light" parent="Material.Drawable.Ripple.Touch.Light"> <item name="fab_radius">28dp</item> <item name="fab_elevation">4dp</item> <item name="fab_iconSize">24dp</item> <item name="fab_interpolator">@android:anim/decelerate_interpolator</item> <item name="fab_animDuration">@android:integer/config_mediumAnimTime</item> </style> <style name="Material.Widget.SnackBar"> <item name="sb_backgroundColor">#FF323232</item> <item name="sb_backgroundCornerRadius">0dp</item> <item name="sb_horizontalPadding">24dp</item> <item name="sb_verticalPadding">0dp</item> <item name="sb_width">match_parent</item> <item name="sb_height">48dp</item> <item name="sb_textAppearance">@style/TextAppearance.AppCompat.Body1</item> <item name="sb_textColor">#FFFFFFFF</item> <item name="sb_ellipsize">end</item> <item name="sb_actionTextAppearance">@style/TextAppearance.AppCompat.Button</item> <item name="sb_actionTextColor">?attr/colorPrimary</item> <item name="sb_actionRipple">@style/Material.Drawable.Ripple.Wave</item> <item name="sb_inAnimation">@anim/abc_slide_in_bottom</item> <item name="sb_outAnimation">@anim/abc_slide_out_bottom</item> <item name="sb_removeOnDismiss">false</item> </style> <style name="Material.Widget.SnackBar.Mobile" parent="Material.Widget.SnackBar"> <item name="sb_marginLeft">0dp</item> <item name="sb_marginBottom">0dp</item> <item name="sb_width">match_parent</item> <item name="sb_height">48dp</item> <item name="sb_singleLine">true</item> </style> <style name="Material.Widget.SnackBar.Mobile.MultiLine" parent="Material.Widget.SnackBar.Mobile"> <item name="sb_verticalPadding">18dp</item> <item name="sb_height">wrap_content</item> <item name="sb_singleLine">false</item> <item name="sb_maxLines">2</item> <item name="sb_maxHeight">80dp</item> </style> <style name="Material.Widget.SnackBar.Tablet" parent="Material.Widget.SnackBar"> <item name="sb_marginLeft">16dp</item> <item name="sb_marginBottom">16dp</item> <item name="sb_width">wrap_content</item> <item name="sb_height">48dp</item> <item name="sb_singleLine">true</item> <item name="sb_minWidth">288dp</item> <item name="sb_maxWidth">568dp</item> </style> <style name="Material.Widget.SnackBar.Tablet.MultiLine" parent="Material.Widget.SnackBar.Tablet"> <item name="sb_verticalPadding">18dp</item> <item name="sb_height">wrap_content</item> <item name="sb_singleLine">false</item> <item name="sb_maxLines">2</item> <item name="sb_maxHeight">80dp</item> </style> <style name="Material.App.Dialog"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">wrap_content</item> <item name="android:windowIsFloating">false</item> <item name="di_dimAmount">0.5</item> <item name="di_backgroundColor">@color/background_floating_material_dark</item> <item name="di_elevation">4dp</item> <item name="di_maxElevation">4dp</item> <item name="di_cornerRadius">2dp</item> <item name="di_titleTextAppearance">@style/TextAppearance.AppCompat.Title</item> <item name="di_titleTextColor">@color/abc_primary_text_material_dark</item> <item name="di_actionRipple">@style/Material.Drawable.Ripple.Wave</item> <item name="di_actionTextAppearance">@style/TextAppearance.AppCompat.Button</item> <item name="di_actionTextColor">?attr/colorControlActivated</item> <item name="di_dividerColor">#1E999999</item> <item name="di_dividerHeight">1dp</item> <item name="di_cancelable">true</item> <item name="di_canceledOnTouchOutside">true</item> </style> <style name="Material.App.Dialog.Light" parent="Material.App.Dialog"> <item name="di_backgroundColor">@color/background_floating_material_light</item> <item name="di_titleTextColor">@color/abc_primary_text_material_light</item> <item name="di_actionRipple">@style/Material.Drawable.Ripple.Wave.Light</item> <item name="di_dividerColor">#1E000000</item> </style> <style name="Material.TextAppearance.SimpleDialog" parent="TextAppearance.AppCompat.Body1"> <item name="android:textColor">@color/abc_secondary_text_material_dark</item> </style> <style name="Material.TextAppearance.SimpleDialog.Light" parent="TextAppearance.AppCompat.Body1"> <item name="android:textColor">@color/abc_secondary_text_material_light</item> </style> <style name="Material.Widget.Spinner"> <item name="spn_labelEnable">false</item> <item name="spn_labelPadding">0dp</item> <item name="spn_labelTextAppearance">@style/TextAppearance.AppCompat.Caption</item> <item name="spn_labelTextColor">@color/abc_secondary_text_material_dark</item> <item name="spn_labelEllipsize">end</item> <item name="spn_dividerHeight">2dp</item> <item name="spn_dividerPadding">0dp</item> <item name="spn_dividerAnimDuration">@android:integer/config_shortAnimTime</item> <item name="spn_arrowColor">?attr/colorControlNormal</item> <item name="spn_arrowSize">4dp</item> <item name="spn_arrowPadding">8dp</item> <item name="spn_popupItemAnimation">@anim/abc_slide_in_bottom</item> <item name="spn_popupItemAnimOffset">120</item> </style> <style name="Material.Widget.Spinner.Light" parent="Material.Widget.Spinner"> <item name="spn_labelTextColor">@color/abc_secondary_text_material_light</item> </style> </resources>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:scrollbarStyle="outsideOverlay"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center" android:padding="8dp"> <com.rey.material.widget.Spinner style="@style/Material.Widget.Spinner.Light" android:id="@+id/spinner_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="128dp" app:rd_style="@style/Material.Drawable.Ripple.Wave.Light" app:rd_enable="true" app:rd_delayClick="false" app:spn_labelEnable="true" app:spn_label="Spinner with arrow" app:spn_arrowSwitchMode="true" app:spn_arrowAnimDuration="@android:integer/config_shortAnimTime" app:spn_arrowInterpolator="@android:anim/decelerate_interpolator"/> <com.rey.material.widget.Spinner style="@style/Material.Widget.Spinner.Light" android:id="@+id/spinner_no_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="128dp" android:layout_marginTop="16dp" app:rd_style="@style/Material.Drawable.Ripple.Wave.Light" app:rd_enable="true" app:rd_delayClick="false" app:spn_labelEnable="true" app:spn_label="Spinner without arrow" app:spn_arrowSize="0dp" app:spn_popupItemAnimation="@anim/abc_fade_in"/> </LinearLayout> </ScrollView>
row_spn_dropdown.xml
<?xml version="1.0" encoding="utf-8"?> <com.rey.material.widget.TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" style="@style/MenuItemRippleStyle" android:id="@+id/row_spn_tv" android:layout_width="match_parent" android:layout_height="48dp" android:textSize="16sp" android:gravity="center_vertical" android:textColor="#FF000000" android:paddingLeft="16dp" android:paddingRight="16dp" app:rd_delayClick="true"/>
row_spn.xml
<?xml version="1.0" encoding="utf-8"?> <com.rey.material.widget.TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" style="@style/MenuItemRippleStyle" android:id="@+id/row_spn_tv" android:layout_width="match_parent" android:layout_height="32dp" android:textSize="16sp" android:gravity="center_vertical" android:textColor="#FF000000" android:paddingLeft="16dp" android:paddingRight="16dp" app:rd_delayClick="true"/>
MainActivity.java
package com.example.spinnersformaterialdesign; import android.app.Activity; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.widget.ArrayAdapter; import com.rey.material.widget.Spinner; public class MainActivity extends Activity { private Drawable[] mDrawables = new Drawable[2]; private int index = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Spinner spn_label = (Spinner)findViewById(R.id.spinner_label); Spinner spn_no_arrow = (Spinner)findViewById(R.id.spinner_no_arrow); ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this, R.layout.row_spn, new String[]{"Item 1", "Item 2", "Item 333333333333333"}); adapter.setDropDownViewResource(R.layout.row_spn_dropdown); spn_label.setAdapter(adapter); spn_no_arrow.setAdapter(adapter); } }
ArrowDrawable.java
package com.rey.material.drawable; import com.rey.material.util.ViewUtil; import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; public class ArrowDrawable extends Drawable implements Animatable{ private boolean mRunning = false; private long mStartTime; private float mAnimProgress; private int mAnimDuration; private Paint mPaint; private ColorStateList mColorStateList; private int mSize; private int mCurColor; private int mMode; private Interpolator mInterpolator; private Path mPath; public static int MODE_DOWN = 0; public static int MODE_UP = 1; private boolean mClockwise = true; public ArrowDrawable(int mode, int size, ColorStateList colorStateList, int animDuration, Interpolator interpolator, boolean clockwise){ mSize = size; mAnimDuration = animDuration; mMode = mode; mInterpolator = interpolator; if(mInterpolator == null) mInterpolator = new DecelerateInterpolator(); mClockwise = clockwise; mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mPath = new Path(); setColor(colorStateList); } public void setColor(ColorStateList colorStateList){ mColorStateList = colorStateList; onStateChange(getState()); } public void setMode(int mode, boolean animation){ if(mMode != mode){ mMode = mode; if(animation && mAnimDuration > 0) start(); else invalidateSelf(); } } public int getMode(){ return mMode; } @Override protected void onBoundsChange(Rect bounds) { float x = bounds.exactCenterX(); float y = bounds.exactCenterY(); mPath.reset(); mPath.moveTo(x, y + mSize / 2f); mPath.lineTo(x - mSize, y - mSize / 2f); mPath.lineTo(x + mSize, y - mSize / 2f); mPath.close(); } @Override public void draw(Canvas canvas) { int saveCount = canvas.save(); Rect bounds = getBounds(); if(!isRunning()){ if(mMode == MODE_UP) canvas.rotate(180, bounds.exactCenterX(), bounds.exactCenterY()); } else{ float value = mInterpolator.getInterpolation(mAnimProgress); float degree; if(mClockwise){ if(mMode == MODE_UP) // move down > up degree = 180 * value; else // move up > down degree = 180 * (1 + value); } else{ if(mMode == MODE_UP) // move down > up degree = -180 * value; else // move up > down degree = -180 * (1 + value); } canvas.rotate(degree, bounds.exactCenterX(), bounds.exactCenterY()); } mPaint.setColor(mCurColor); canvas.drawPath(mPath, mPaint); canvas.restoreToCount(saveCount); } @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; } @Override public boolean isStateful() { return true; } @Override protected boolean onStateChange(int[] state) { int color = mColorStateList.getColorForState(state, mCurColor); if(mCurColor != color){ mCurColor = color; return true; } return false; } private void resetAnimation(){ mStartTime = SystemClock.uptimeMillis(); mAnimProgress = 0f; } @Override public void start() { resetAnimation(); scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } @Override public void stop() { mRunning = false; unscheduleSelf(mUpdater); invalidateSelf(); } @Override public boolean isRunning() { return mRunning; } @Override public void scheduleSelf(Runnable what, long when) { mRunning = true; super.scheduleSelf(what, when); } 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) mRunning = false; if(isRunning()) scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } }
DividerDrawable.java
package com.rey.material.drawable; import android.content.res.ColorStateList; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathEffect; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.SystemClock; import com.rey.material.util.ViewUtil; public class DividerDrawable extends Drawable implements Animatable{ private boolean mRunning = false; private long mStartTime; private float mAnimProgress; private int mAnimDuration; private Paint mPaint; private ColorStateList mColorStateList; private int mHeight; private int mPrevColor; private int mCurColor; private boolean mEnable = true; private PathEffect mPathEffect; private Path mPath; private boolean mInEditMode = false; private boolean mAnimEnable = true; private int mPaddingLeft; private int mPaddingRight; public DividerDrawable(int height, ColorStateList colorStateList, int animDuration){ this(height, 0, 0, colorStateList, animDuration); } public DividerDrawable(int height, int paddingLeft, int paddingRight, ColorStateList colorStateList, int animDuration){ mHeight = height; mPaddingLeft = paddingLeft; mPaddingRight = paddingRight; mAnimDuration = animDuration; mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mHeight); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); mPath = new Path(); mAnimEnable = false; setColor(colorStateList); mAnimEnable = true; } public void setPadding(int left, int right){ if(mPaddingLeft != left || mPaddingRight != right){ mPaddingLeft = left; mPaddingRight = right; invalidateSelf(); } } public int getPaddingLeft(){ return mPaddingLeft; } public int getPaddingRight(){ return mPaddingRight; } public void setInEditMode(boolean b){ mInEditMode = b; } public void setAnimEnable(boolean b){ mAnimEnable = b; } public void setColor(ColorStateList colorStateList){ mColorStateList = colorStateList; onStateChange(getState()); } private PathEffect getPathEffect(){ if(mPathEffect == null) mPathEffect = new DashPathEffect(new float[]{0.2f, mHeight * 2}, 0f); return mPathEffect; } @Override public void draw(Canvas canvas) { if(mHeight == 0) return; Rect bounds = getBounds(); float y = bounds.bottom - mHeight / 2; if(!isRunning()){ mPath.reset(); mPath.moveTo(bounds.left + mPaddingLeft, y); mPath.lineTo(bounds.right - mPaddingRight, y); mPaint.setPathEffect(mEnable ? null : getPathEffect()); mPaint.setColor(mCurColor); canvas.drawPath(mPath, mPaint); } else{ float centerX = (bounds.right + bounds.left - mPaddingRight + mPaddingLeft) / 2f; float start = centerX * (1f - mAnimProgress) + (bounds.left + mPaddingLeft) * mAnimProgress; float end = centerX * (1f - mAnimProgress) + (bounds.right + mPaddingRight) * mAnimProgress; mPaint.setPathEffect(null); if(mAnimProgress < 1f){ mPaint.setColor(mPrevColor); mPath.reset(); mPath.moveTo(bounds.left + mPaddingLeft, y); mPath.lineTo(start, y); mPath.moveTo(bounds.right - mPaddingRight, y); mPath.lineTo(end, y); canvas.drawPath(mPath, mPaint); } mPaint.setColor(mCurColor); mPath.reset(); mPath.moveTo(start, y); mPath.lineTo(end, y); canvas.drawPath(mPath, 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; } @Override public boolean isStateful() { return true; } @Override protected boolean onStateChange(int[] state) { mEnable = ViewUtil.hasState(state, android.R.attr.state_enabled); int color = mColorStateList.getColorForState(state, mCurColor); if(mCurColor != color){ if(!mInEditMode && mAnimEnable && mEnable){ mPrevColor = isRunning() ? mPrevColor : mCurColor; mCurColor = color; start(); } else{ mPrevColor = color; mCurColor = color; } return true; } else if(!isRunning()) mPrevColor = color; return false; } private void resetAnimation(){ mStartTime = SystemClock.uptimeMillis(); mAnimProgress = 0f; } @Override public void start() { resetAnimation(); scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } @Override public void stop() { mRunning = false; unscheduleSelf(mUpdater); invalidateSelf(); } @Override public boolean isRunning() { return mRunning; } @Override public void scheduleSelf(Runnable what, long when) { mRunning = true; super.scheduleSelf(what, when); } 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) mRunning = false; if(isRunning()) scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } }
RippleDrawable.java
package com.rey.material.drawable; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Path.Direction; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.example.spinnersformaterialdesign.R; import com.rey.material.util.ColorUtil; import com.rey.material.util.ThemeUtil; import com.rey.material.util.ViewUtil; public class RippleDrawable extends Drawable implements Animatable, OnTouchListener { private boolean mRunning = false; private Paint mShaderPaint; private Paint mFillPaint; private Mask mMask; private RadialGradient mInShader; private RadialGradient mOutShader; private Matrix mMatrix; private int mAlpha = 255; private Drawable mBackgroundDrawable; private RectF mBackgroundBounds; private Path mBackground; private int mBackgroundAnimDuration; private int mBackgroundColor; private float mBackgroundAlphaPercent; private PointF mRipplePoint; private float mRippleRadius; private int mRippleType; private int mMaxRippleRadius; private int mRippleAnimDuration; private int mRippleColor; private float mRippleAlphaPercent; private boolean mDelayClick; private Interpolator mInInterpolator; private Interpolator mOutInterpolator; private long mStartTime; private int mState = STATE_OUT; private static final int STATE_OUT = 0; private static final int STATE_PRESS = 1; private static final int STATE_HOVER = 2; private static final int STATE_RELEASE_ON_HOLD = 3; private static final int STATE_RELEASE = 4; private static final int TYPE_TOUCH_MATCH_VIEW = -1; private static final int TYPE_TOUCH = 0; private static final int TYPE_WAVE = 1; private static final float[] GRADIENT_STOPS = new float[]{0f, 0.99f, 1f}; private static final float GRADIENT_RADIUS = 16; private RippleDrawable(Drawable backgroundDrawable, int backgroundAnimDuration, int backgroundColor, int rippleType, boolean delayClick, int maxRippleRadius, int rippleAnimDuration, int rippleColor, Interpolator inInterpolator, Interpolator outInterpolator, int type, int topLeftCornerRadius, int topRightCornerRadius, int bottomRightCornerRadius, int bottomLeftCornerRadius, int left, int top, int right, int bottom){ setBackgroundDrawable(backgroundDrawable); mBackgroundAnimDuration = backgroundAnimDuration; mBackgroundColor = backgroundColor; mRippleType = rippleType; setDelayClick(delayClick); mMaxRippleRadius = maxRippleRadius; mRippleAnimDuration = rippleAnimDuration; mRippleColor = rippleColor; if(mRippleType == TYPE_TOUCH && mMaxRippleRadius <= 0) mRippleType = TYPE_TOUCH_MATCH_VIEW; mInInterpolator = inInterpolator; mOutInterpolator = outInterpolator; setMask(type, topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius, left, top, right, bottom); mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFillPaint.setStyle(Paint.Style.FILL); mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mShaderPaint.setStyle(Paint.Style.FILL); mBackground = new Path(); mBackgroundBounds = new RectF(); mRipplePoint = new PointF(); mMatrix = new Matrix(); mInShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{mRippleColor, mRippleColor, 0}, GRADIENT_STOPS, Shader.TileMode.CLAMP); if(mRippleType == TYPE_WAVE) mOutShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{0, ColorUtil.getColor(mRippleColor, 0f), mRippleColor}, GRADIENT_STOPS, Shader.TileMode.CLAMP); } public void setBackgroundDrawable(Drawable backgroundDrawable){ mBackgroundDrawable = backgroundDrawable; if(mBackgroundDrawable != null) mBackgroundDrawable.setBounds(getBounds()); } public boolean isDelayClick(){ return mDelayClick; } public void setDelayClick(boolean enable){ mDelayClick = enable; } public void setMask(int type, int topLeftCornerRadius, int topRightCornerRadius, int bottomRightCornerRadius, int bottomLeftCornerRadius, int left, int top, int right, int bottom){ mMask = new Mask(type, topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius, left, top, right, bottom); } @Override public void setAlpha(int alpha) { mAlpha = alpha; } @Override public void setColorFilter(ColorFilter filter) { mFillPaint.setColorFilter(filter); mShaderPaint.setColorFilter(filter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } public long getClickDelayTime(){ if(mState == STATE_RELEASE_ON_HOLD) return (mDelayClick ? 2 : 1) * Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); else if(mState == STATE_RELEASE) return (mDelayClick ? Math.max(mBackgroundAnimDuration, mRippleAnimDuration) : 0) - (SystemClock.uptimeMillis() - mStartTime); return -1; } private void setRippleState(int state){ if(mState != state){ mState = state; if(mState != STATE_OUT){ if(mState != STATE_HOVER) start(); else stop(); } else stop(); } } private boolean setRippleEffect(float x, float y, float radius){ if(mRipplePoint.x != x || mRipplePoint.y != y || mRippleRadius != radius){ mRipplePoint.set(x, y); mRippleRadius = radius; radius = mRippleRadius / GRADIENT_RADIUS; mMatrix.reset(); mMatrix.postTranslate(x, y); mMatrix.postScale(radius, radius, x, y); mInShader.setLocalMatrix(mMatrix); if(mOutShader != null) mOutShader.setLocalMatrix(mMatrix); return true; } return false; } @Override protected void onBoundsChange(Rect bounds) { if(mBackgroundDrawable != null) mBackgroundDrawable.setBounds(bounds); mBackgroundBounds.set(bounds.left + mMask.left, bounds.top + mMask.top, bounds.right - mMask.right, bounds.bottom - mMask.bottom); mBackground.reset(); switch (mMask.type) { case Mask.TYPE_OVAL: mBackground.addOval(mBackgroundBounds, Direction.CW); break; case Mask.TYPE_RECTANGLE: mBackground.addRoundRect(mBackgroundBounds, mMask.cornerRadius, Direction.CW); break; } } @Override public boolean isStateful() { return mBackgroundDrawable != null && mBackgroundDrawable.isStateful(); } @Override protected boolean onStateChange(int[] state) { return mBackgroundDrawable != null && mBackgroundDrawable.setState(state); } @Override public void draw(Canvas canvas) { if(mBackgroundDrawable != null) mBackgroundDrawable.draw(canvas); switch (mRippleType) { case TYPE_TOUCH: case TYPE_TOUCH_MATCH_VIEW: drawTouch(canvas); break; case TYPE_WAVE: drawWave(canvas); break; } } private void drawTouch(Canvas canvas){ if(mState != STATE_OUT){ if(mBackgroundAlphaPercent > 0){ mFillPaint.setColor(mBackgroundColor); mFillPaint.setAlpha(Math.round(mAlpha * mBackgroundAlphaPercent)); canvas.drawPath(mBackground, mFillPaint); } if(mRippleRadius > 0 && mRippleAlphaPercent > 0){ mShaderPaint.setAlpha(Math.round(mAlpha * mRippleAlphaPercent)); mShaderPaint.setShader(mInShader); canvas.drawPath(mBackground, mShaderPaint); } } } private void drawWave(Canvas canvas){ if(mState != STATE_OUT){ if(mState == STATE_RELEASE){ if(mRippleRadius == 0){ mFillPaint.setColor(mRippleColor); canvas.drawPath(mBackground, mFillPaint); } else{ mShaderPaint.setShader(mOutShader); canvas.drawPath(mBackground, mShaderPaint); } } else if(mRippleRadius > 0){ mShaderPaint.setShader(mInShader); canvas.drawPath(mBackground, mShaderPaint); } } } private int getMaxRippleRadius(float x, float y){ float x1 = x < mBackgroundBounds.centerX() ? mBackgroundBounds.right : mBackgroundBounds.left; float y1 = y < mBackgroundBounds.centerY() ? mBackgroundBounds.bottom : mBackgroundBounds.top; return (int)Math.round(Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2))); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: if(mState == STATE_OUT || mState == STATE_RELEASE){ if(mRippleType == TYPE_WAVE || mRippleType == TYPE_TOUCH_MATCH_VIEW) mMaxRippleRadius = getMaxRippleRadius(event.getX(), event.getY()); setRippleEffect(event.getX(), event.getY(), 0); setRippleState(STATE_PRESS); } else if(mRippleType == TYPE_TOUCH){ if(setRippleEffect(event.getX(), event.getY(), mRippleRadius)) invalidateSelf(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if(mState != STATE_OUT){ if(mState == STATE_HOVER){ if(mRippleType == TYPE_WAVE || mRippleType == TYPE_TOUCH_MATCH_VIEW) setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); setRippleState(STATE_RELEASE); } else setRippleState(STATE_RELEASE_ON_HOLD); } break; } return true; } //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ public void cancel(){ setRippleState(STATE_OUT); } private void resetAnimation(){ mStartTime = SystemClock.uptimeMillis(); } @Override public void start() { if(isRunning()) return; resetAnimation(); scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } @Override public void stop() { if(!isRunning()) return; mRunning = false; unscheduleSelf(mUpdater); invalidateSelf(); } @Override public boolean isRunning() { return mRunning; } @Override public void scheduleSelf(Runnable what, long when) { mRunning = true; super.scheduleSelf(what, when); } private final Runnable mUpdater = new Runnable() { @Override public void run() { switch (mRippleType) { case TYPE_TOUCH: case TYPE_TOUCH_MATCH_VIEW: updateTouch(); break; case TYPE_WAVE: updateWave(); break; } } }; private void updateTouch(){ if(mState != STATE_RELEASE){ float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); mBackgroundAlphaPercent = mInInterpolator.getInterpolation(backgroundProgress) * Color.alpha(mBackgroundColor) / 255f; float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); mRippleAlphaPercent = mInInterpolator.getInterpolation(touchProgress); setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(touchProgress)); if(backgroundProgress == 1f && touchProgress == 1f){ mStartTime = SystemClock.uptimeMillis(); setRippleState(mState == STATE_PRESS ? STATE_HOVER : STATE_RELEASE); } } else{ float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); mBackgroundAlphaPercent = (1f - mOutInterpolator.getInterpolation(backgroundProgress)) * Color.alpha(mBackgroundColor) / 255f; float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); mRippleAlphaPercent = 1f - mOutInterpolator.getInterpolation(touchProgress); setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * (1f + 0.5f * mOutInterpolator.getInterpolation(touchProgress))); if(backgroundProgress == 1f && touchProgress == 1f) setRippleState(STATE_OUT); } if(isRunning()) scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } private void updateWave(){ float progress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); if(mState != STATE_RELEASE){ setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(progress)); if(progress == 1f){ mStartTime = SystemClock.uptimeMillis(); if(mState == STATE_PRESS) setRippleState(STATE_HOVER); else{ setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); setRippleState(STATE_RELEASE); } } } else{ setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mOutInterpolator.getInterpolation(progress)); if(progress == 1f) setRippleState(STATE_OUT); } if(isRunning()) scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } public static class Mask{ public static final int TYPE_RECTANGLE = 0; public static final int TYPE_OVAL = 1; final int type; final float[] cornerRadius = new float[8]; final int left; final int top; final int right; final int bottom; public Mask(int type, int topLeftCornerRadius, int topRightCornerRadius, int bottomRightCornerRadius, int bottomLeftCornerRadius, int left, int top, int right, int bottom){ this.type = type; cornerRadius[0] = topLeftCornerRadius; cornerRadius[1] = topLeftCornerRadius; cornerRadius[2] = topRightCornerRadius; cornerRadius[3] = topRightCornerRadius; cornerRadius[4] = bottomRightCornerRadius; cornerRadius[5] = bottomRightCornerRadius; cornerRadius[6] = bottomLeftCornerRadius; cornerRadius[7] = bottomLeftCornerRadius; this.left = left; this.top = top; this.right = right; this.bottom = bottom; } } public static class Builder{ private Drawable mBackgroundDrawable; private int mBackgroundAnimDuration = 200; private int mBackgroundColor; private int mRippleType; private int mMaxRippleRadius; private int mRippleAnimDuration = 400; private int mRippleColor; private boolean mDelayClick; private Interpolator mInInterpolator; private Interpolator mOutInterpolator; private int mMaskType; private int mMaskTopLeftCornerRadius; private int mMaskTopRightCornerRadius; private int mMaskBottomLeftCornerRadius; private int mMaskBottomRightCornerRadius; private int mMaskLeft; private int mMaskTop; private int mMaskRight; private int mMaskBottom; public Builder(){} public Builder(Context context, int defStyleRes){ this(context, null, 0, defStyleRes); } public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleDrawable, defStyleAttr, defStyleRes); int type, resId; backgroundColor(a.getColor(R.styleable.RippleDrawable_rd_backgroundColor, 0)); backgroundAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_backgroundAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); rippleType(a.getInteger(R.styleable.RippleDrawable_rd_rippleType, RippleDrawable.TYPE_TOUCH)); delayClick(a.getBoolean(R.styleable.RippleDrawable_rd_delayClick, false)); type = ThemeUtil.getType(a, R.styleable.RippleDrawable_rd_maxRippleRadius); if(type >= TypedValue.TYPE_FIRST_INT && type <= TypedValue.TYPE_LAST_INT) maxRippleRadius(a.getInteger(R.styleable.RippleDrawable_rd_maxRippleRadius, -1)); else maxRippleRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_maxRippleRadius, ThemeUtil.dpToPx(context, 48))); rippleColor(a.getColor(R.styleable.RippleDrawable_rd_rippleColor, ThemeUtil.colorControlHighlight(context, 0))); rippleAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_rippleAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_inInterpolator, 0)) != 0) inInterpolator(AnimationUtils.loadInterpolator(context, resId)); if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_outInterpolator, 0)) != 0) outInterpolator(AnimationUtils.loadInterpolator(context, resId)); maskType(a.getInteger(R.styleable.RippleDrawable_rd_maskType, Mask.TYPE_RECTANGLE)); cornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_cornerRadius, 0)); topLeftCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_topLeftCornerRadius, mMaskTopLeftCornerRadius)); topRightCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_topRightCornerRadius, mMaskTopRightCornerRadius)); bottomRightCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_bottomRightCornerRadius, mMaskBottomRightCornerRadius)); bottomLeftCornerRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_bottomLeftCornerRadius, mMaskBottomLeftCornerRadius)); padding(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_padding, 0)); left(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_leftPadding, mMaskLeft)); right(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_rightPadding, mMaskRight)); top(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_topPadding, mMaskTop)); bottom(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_bottomPadding, mMaskBottom)); a.recycle(); } public RippleDrawable build(){ if(mInInterpolator == null) mInInterpolator = new AccelerateInterpolator(); if(mOutInterpolator == null) mOutInterpolator = new DecelerateInterpolator(); return new RippleDrawable(mBackgroundDrawable, mBackgroundAnimDuration, mBackgroundColor, mRippleType, mDelayClick, mMaxRippleRadius, mRippleAnimDuration, mRippleColor, mInInterpolator, mOutInterpolator, mMaskType, mMaskTopLeftCornerRadius, mMaskTopRightCornerRadius, mMaskBottomRightCornerRadius, mMaskBottomLeftCornerRadius, mMaskLeft, mMaskTop, mMaskRight, mMaskBottom); } public Builder backgroundDrawable(Drawable drawable){ mBackgroundDrawable = drawable; return this; } public Builder backgroundAnimDuration(int duration){ mBackgroundAnimDuration = duration; return this; } public Builder backgroundColor(int color){ mBackgroundColor = color; return this; } public Builder rippleType(int type){ mRippleType = type; return this; } public Builder delayClick(boolean enable){ mDelayClick = enable; return this; } public Builder maxRippleRadius(int radius){ mMaxRippleRadius = radius; return this; } public Builder rippleAnimDuration(int duration){ mRippleAnimDuration = duration; return this; } public Builder rippleColor(int color){ mRippleColor = color; return this; } public Builder inInterpolator(Interpolator interpolator){ mInInterpolator = interpolator; return this; } public Builder outInterpolator(Interpolator interpolator){ mOutInterpolator = interpolator; return this; } public Builder maskType(int type){ mMaskType = type; return this; } public Builder cornerRadius(int radius){ mMaskTopLeftCornerRadius = radius; mMaskTopRightCornerRadius = radius; mMaskBottomLeftCornerRadius = radius; mMaskBottomRightCornerRadius = radius; return this; } public Builder topLeftCornerRadius(int radius){ mMaskTopLeftCornerRadius = radius; return this; } public Builder topRightCornerRadius(int radius){ mMaskTopRightCornerRadius = radius; return this; } public Builder bottomLeftCornerRadius(int radius){ mMaskBottomLeftCornerRadius = radius; return this; } public Builder bottomRightCornerRadius(int radius){ mMaskBottomRightCornerRadius = radius; return this; } public Builder padding(int padding){ mMaskLeft = padding; mMaskTop = padding; mMaskRight = padding; mMaskBottom = padding; return this; } public Builder left(int padding){ mMaskLeft = padding; return this; } public Builder top(int padding){ mMaskTop = padding; return this; } public Builder right(int padding){ mMaskRight = padding; return this; } public Builder bottom(int padding){ mMaskBottom = padding; return this; } } }
ToolbarRippleDrawable.java
package com.rey.material.drawable; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Path.Direction; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.util.AttributeSet; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.example.spinnersformaterialdesign.R; import com.rey.material.util.ColorUtil; import com.rey.material.util.ThemeUtil; import com.rey.material.util.ViewUtil; public class ToolbarRippleDrawable extends Drawable implements Animatable { private boolean mRunning = false; private Paint mShaderPaint; private Paint mFillPaint; private RadialGradient mInShader; private RadialGradient mOutShader; private Matrix mMatrix; private int mAlpha = 255; private RectF mBackgroundBounds; private Path mBackground; private int mBackgroundAnimDuration; private int mBackgroundColor; private float mBackgroundAlphaPercent; private PointF mRipplePoint; private float mRippleRadius; private int mRippleType; private int mMaxRippleRadius; private int mRippleAnimDuration; private int mRippleColor; private float mRippleAlphaPercent; private boolean mDelayClick; private Interpolator mInInterpolator; private Interpolator mOutInterpolator; private long mStartTime; private boolean mPressed = false; private int mState = STATE_OUT; private static final int STATE_OUT = 0; private static final int STATE_PRESS = 1; private static final int STATE_HOVER = 2; private static final int STATE_RELEASE_ON_HOLD = 3; private static final int STATE_RELEASE = 4; private static final int TYPE_TOUCH_MATCH_VIEW = -1; private static final int TYPE_TOUCH = 0; private static final int TYPE_WAVE = 1; private static final float[] GRADIENT_STOPS = new float[]{0f, 0.99f, 1f}; private static final float GRADIENT_RADIUS = 16; private ToolbarRippleDrawable(int backgroundAnimDuration, int backgroundColor, int rippleType, boolean delayClick, int maxTouchRadius, int touchAnimDuration, int touchColor, Interpolator inInterpolator, Interpolator outInterpolator){ mBackgroundAnimDuration = backgroundAnimDuration; mBackgroundColor = backgroundColor; mRippleType = rippleType; mMaxRippleRadius = maxTouchRadius; mRippleAnimDuration = touchAnimDuration; mRippleColor = touchColor; mDelayClick = delayClick; if(mRippleType == TYPE_TOUCH && mMaxRippleRadius <= 0) mRippleType = TYPE_TOUCH_MATCH_VIEW; mInInterpolator = inInterpolator; mOutInterpolator = outInterpolator; mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFillPaint.setStyle(Paint.Style.FILL); mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mShaderPaint.setStyle(Paint.Style.FILL); mBackground = new Path(); mBackgroundBounds = new RectF(); mRipplePoint = new PointF(); mMatrix = new Matrix(); mInShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{mRippleColor, mRippleColor, 0}, GRADIENT_STOPS, Shader.TileMode.CLAMP); if(mRippleType == TYPE_WAVE) mOutShader = new RadialGradient(0, 0, GRADIENT_RADIUS, new int[]{0, ColorUtil.getColor(mRippleColor, 0f), mRippleColor}, GRADIENT_STOPS, Shader.TileMode.CLAMP); } public boolean isDelayClick(){ return mDelayClick; } public void setDelayClick(boolean enable){ mDelayClick = enable; } @Override public void setAlpha(int alpha) { mAlpha = alpha; } @Override public void setColorFilter(ColorFilter filter) { mFillPaint.setColorFilter(filter); mShaderPaint.setColorFilter(filter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } public long getClickDelayTime(){ if(mState == STATE_RELEASE_ON_HOLD) return (mDelayClick ? 2 : 1) * Math.max(mBackgroundAnimDuration, mRippleAnimDuration) - (SystemClock.uptimeMillis() - mStartTime); else if(mState == STATE_RELEASE) return (mDelayClick ? Math.max(mBackgroundAnimDuration, mRippleAnimDuration) : 0) - (SystemClock.uptimeMillis() - mStartTime); return -1; } private void setRippleState(int state){ if(mState != state){ mState = state; if(mState != STATE_OUT){ if(mState != STATE_HOVER) start(); else stop(); } else stop(); } } private boolean setRippleEffect(float x, float y, float radius){ if(mRipplePoint.x != x || mRipplePoint.y != y || mRippleRadius != radius){ mRipplePoint.set(x, y); mRippleRadius = radius; radius = mRippleRadius / GRADIENT_RADIUS; mMatrix.reset(); mMatrix.postTranslate(x, y); mMatrix.postScale(radius, radius, x, y); mInShader.setLocalMatrix(mMatrix); if(mOutShader != null) mOutShader.setLocalMatrix(mMatrix); return true; } return false; } @Override protected void onBoundsChange(Rect bounds) { mBackgroundBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom); mBackground.reset(); mBackground.addRect(mBackgroundBounds, Direction.CW); } @Override public boolean isStateful() { return true; } @Override protected boolean onStateChange(int[] state) { boolean pressed = ViewUtil.hasState(state, android.R.attr.state_pressed); if(mPressed != pressed){ mPressed = pressed; if(mPressed){ Rect bounds = getBounds(); if(mState == STATE_OUT || mState == STATE_RELEASE){ if(mRippleType == TYPE_WAVE || mRippleType == TYPE_TOUCH_MATCH_VIEW) mMaxRippleRadius = getMaxRippleRadius(bounds.exactCenterX(), bounds.exactCenterY()); setRippleEffect(bounds.exactCenterX(), bounds.exactCenterY(), 0); setRippleState(STATE_PRESS); } else if(mRippleType == TYPE_TOUCH) setRippleEffect(bounds.exactCenterX(), bounds.exactCenterY(), mRippleRadius); } else{ if(mState != STATE_OUT){ if(mState == STATE_HOVER){ if(mRippleType == TYPE_WAVE|| mRippleType == TYPE_TOUCH_MATCH_VIEW) setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); setRippleState(STATE_RELEASE); } else setRippleState(STATE_RELEASE_ON_HOLD); } } return true; } return false; } @Override public void draw(Canvas canvas) { switch (mRippleType) { case TYPE_TOUCH: case TYPE_TOUCH_MATCH_VIEW: drawTouch(canvas); break; case TYPE_WAVE: drawWave(canvas); break; } } private void drawTouch(Canvas canvas){ if(mState != STATE_OUT){ if(mBackgroundAlphaPercent > 0){ mFillPaint.setColor(mBackgroundColor); mFillPaint.setAlpha(Math.round(mAlpha * mBackgroundAlphaPercent)); canvas.drawPath(mBackground, mFillPaint); } if(mRippleRadius > 0 && mRippleAlphaPercent > 0){ mShaderPaint.setAlpha(Math.round(mAlpha * mRippleAlphaPercent)); mShaderPaint.setShader(mInShader); canvas.drawPath(mBackground, mShaderPaint); } } } private void drawWave(Canvas canvas){ if(mState != STATE_OUT){ if(mState == STATE_RELEASE){ if(mRippleRadius == 0){ mFillPaint.setColor(mRippleColor); canvas.drawPath(mBackground, mFillPaint); } else{ mShaderPaint.setShader(mOutShader); canvas.drawPath(mBackground, mShaderPaint); } } else if(mRippleRadius > 0){ mShaderPaint.setShader(mInShader); canvas.drawPath(mBackground, mShaderPaint); } } } private int getMaxRippleRadius(float x, float y){ float x1 = x < mBackgroundBounds.centerX() ? mBackgroundBounds.right : mBackgroundBounds.left; float y1 = y < mBackgroundBounds.centerY() ? mBackgroundBounds.bottom : mBackgroundBounds.top; return (int)Math.round(Math.sqrt(Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2))); } //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/ public void cancel(){ setRippleState(STATE_OUT); } private void resetAnimation(){ mStartTime = SystemClock.uptimeMillis(); } @Override public void start() { if(isRunning()) return; resetAnimation(); scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } @Override public void stop() { if(!isRunning()) return; mRunning = false; unscheduleSelf(mUpdater); invalidateSelf(); } @Override public boolean isRunning() { return mRunning; } @Override public void scheduleSelf(Runnable what, long when) { mRunning = true; super.scheduleSelf(what, when); } private final Runnable mUpdater = new Runnable() { @Override public void run() { switch (mRippleType) { case TYPE_TOUCH: case TYPE_TOUCH_MATCH_VIEW: updateTouch(); break; case TYPE_WAVE: updateWave(); break; } } }; private void updateTouch(){ if(mState != STATE_RELEASE){ float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); mBackgroundAlphaPercent = mInInterpolator.getInterpolation(backgroundProgress) * Color.alpha(mBackgroundColor) / 255f; float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); mRippleAlphaPercent = mInInterpolator.getInterpolation(touchProgress); setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(touchProgress)); if(backgroundProgress == 1f && touchProgress == 1f){ mStartTime = SystemClock.uptimeMillis(); setRippleState(mState == STATE_PRESS ? STATE_HOVER : STATE_RELEASE); } } else{ float backgroundProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mBackgroundAnimDuration); mBackgroundAlphaPercent = (1f - mOutInterpolator.getInterpolation(backgroundProgress)) * Color.alpha(mBackgroundColor) / 255f; float touchProgress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); mRippleAlphaPercent = 1f - mOutInterpolator.getInterpolation(touchProgress); setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * (1f + 0.5f * mOutInterpolator.getInterpolation(touchProgress))); if(backgroundProgress == 1f && touchProgress == 1f) setRippleState(STATE_OUT); } if(isRunning()) scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } private void updateWave(){ float progress = Math.min(1f, (float)(SystemClock.uptimeMillis() - mStartTime) / mRippleAnimDuration); if(mState != STATE_RELEASE){ setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mInInterpolator.getInterpolation(progress)); if(progress == 1f){ mStartTime = SystemClock.uptimeMillis(); if(mState == STATE_PRESS) setRippleState(STATE_HOVER); else{ setRippleEffect(mRipplePoint.x, mRipplePoint.y, 0); setRippleState(STATE_RELEASE); } } } else{ setRippleEffect(mRipplePoint.x, mRipplePoint.y, mMaxRippleRadius * mOutInterpolator.getInterpolation(progress)); if(progress == 1f) setRippleState(STATE_OUT); } if(isRunning()) scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); invalidateSelf(); } public static class Builder{ private int mBackgroundAnimDuration = 200; private int mBackgroundColor; private int mRippleType; private int mMaxRippleRadius; private int mRippleAnimDuration = 400; private int mRippleColor; private boolean mDelayClick; private Interpolator mInInterpolator; private Interpolator mOutInterpolator; public Builder(){} public Builder(Context context, int defStyleRes){ this(context, null, 0, defStyleRes); } public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleDrawable, defStyleAttr, defStyleRes); int resId; backgroundColor(a.getColor(R.styleable.RippleDrawable_rd_backgroundColor, 0)); backgroundAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_backgroundAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); rippleType(a.getInteger(R.styleable.RippleDrawable_rd_rippleType, ToolbarRippleDrawable.TYPE_TOUCH)); delayClick(a.getBoolean(R.styleable.RippleDrawable_rd_delayClick, false)); maxRippleRadius(a.getDimensionPixelSize(R.styleable.RippleDrawable_rd_maxRippleRadius, ThemeUtil.dpToPx(context, 48))); rippleColor(a.getColor(R.styleable.RippleDrawable_rd_rippleColor, ThemeUtil.colorControlHighlight(context, 0))); rippleAnimDuration(a.getInteger(R.styleable.RippleDrawable_rd_rippleAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime))); if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_inInterpolator, 0)) != 0) inInterpolator(AnimationUtils.loadInterpolator(context, resId)); if((resId = a.getResourceId(R.styleable.RippleDrawable_rd_outInterpolator, 0)) != 0) outInterpolator(AnimationUtils.loadInterpolator(context, resId)); a.recycle(); } public ToolbarRippleDrawable build(){ if(mInInterpolator == null) mInInterpolator = new AccelerateInterpolator(); if(mOutInterpolator == null) mOutInterpolator = new DecelerateInterpolator(); return new ToolbarRippleDrawable(mBackgroundAnimDuration, mBackgroundColor, mRippleType, mDelayClick, mMaxRippleRadius, mRippleAnimDuration, mRippleColor, mInInterpolator, mOutInterpolator); } public Builder backgroundAnimDuration(int duration){ mBackgroundAnimDuration = duration; return this; } public Builder backgroundColor(int color){ mBackgroundColor = color; return this; } public Builder rippleType(int type){ mRippleType = type; return this; } public Builder delayClick(boolean enable){ mDelayClick = enable; return this; } public Builder maxRippleRadius(int radius){ mMaxRippleRadius = radius; return this; } public Builder rippleAnimDuration(int duration){ mRippleAnimDuration = duration; return this; } public Builder rippleColor(int color){ mRippleColor = color; return this; } public Builder inInterpolator(Interpolator interpolator){ mInInterpolator = interpolator; return this; } public Builder outInterpolator(Interpolator interpolator){ mOutInterpolator = interpolator; return this; } } }
ColorUtil.java
package com.rey.material.util; import android.graphics.Color; public class ColorUtil { private static int getMiddleValue(int prev, int next, float factor){ return Math.round(prev + (next - prev) * factor); } public static int getMiddleColor(int prevColor, int curColor, float factor){ if(prevColor == curColor) return curColor; if(factor == 0f) return prevColor; else if(factor == 1f) return curColor; int a = getMiddleValue(Color.alpha(prevColor), Color.alpha(curColor), factor); int r = getMiddleValue(Color.red(prevColor), Color.red(curColor), factor); int g = getMiddleValue(Color.green(prevColor), Color.green(curColor), factor); int b = getMiddleValue(Color.blue(prevColor), Color.blue(curColor), factor); return Color.argb(a, r, g, b); } public static int getColor(int baseColor, float alphaPercent){ int alpha = Math.round(Color.alpha(baseColor) * alphaPercent); return (baseColor & 0x00FFFFFF) | (alpha << 24); } }
ThemeUtil.java
package com.rey.material.util; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.os.Build; import android.support.v7.internal.widget.TintTypedArray; import android.util.TypedValue; import com.example.spinnersformaterialdesign.R; public class ThemeUtil { private static TypedValue value; public static int dpToPx(Context context, int dp){ return (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()) + 0.5f); } public static int spToPx(Context context, int sp){ return (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, context.getResources().getDisplayMetrics()) + 0.5f); } private static int getColor(Context context, int id, int defaultValue){ if(value == null) value = new TypedValue(); try{ Theme theme = context.getTheme(); if(theme != null && theme.resolveAttribute(id, value, true)){ if (value.type >= TypedValue.TYPE_FIRST_INT && value.type <= TypedValue.TYPE_LAST_INT) return value.data; else if (value.type == TypedValue.TYPE_STRING) return context.getResources().getColor(value.resourceId); } } catch(Exception ex){} return defaultValue; } public static int windowBackground(Context context, int defaultValue){ return getColor(context, android.R.attr.windowBackground, defaultValue); } public static int textColorPrimary(Context context, int defaultValue){ return getColor(context, android.R.attr.textColorPrimary, defaultValue); } public static int textColorSecondary(Context context, int defaultValue){ return getColor(context, android.R.attr.textColorSecondary, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorPrimary(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorPrimary, defaultValue); return getColor(context, R.attr.colorPrimary, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorPrimaryDark(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorPrimaryDark, defaultValue); return getColor(context, R.attr.colorPrimaryDark, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorAccent(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorAccent, defaultValue); return getColor(context, R.attr.colorAccent, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorControlNormal(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorControlNormal, defaultValue); return getColor(context, R.attr.colorControlNormal, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorControlActivated(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorControlActivated, defaultValue); return getColor(context, R.attr.colorControlActivated, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorControlHighlight(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorControlHighlight, defaultValue); return getColor(context, R.attr.colorControlHighlight, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorButtonNormal(Context context, int defaultValue){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return getColor(context, android.R.attr.colorButtonNormal, defaultValue); return getColor(context, R.attr.colorButtonNormal, defaultValue); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static int colorSwitchThumbNormal(Context context, int defaultValue){ return getColor(context, R.attr.colorSwitchThumbNormal, defaultValue); } public static int getType(TypedArray array, int index){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) return array.getType(index); else{ TypedValue value = array.peekValue(index); return value == null ? TypedValue.TYPE_NULL : value.type; } } public static CharSequence getString(TypedArray array, int index, CharSequence defaultValue){ String result = array.getString(index); return result == null ? defaultValue : result; } public static CharSequence getString(TintTypedArray array, int index, CharSequence defaultValue){ String result = array.getString(index); return result == null ? defaultValue : result; } }
TypefaceUtil.java
package com.rey.material.util; import android.content.Context; import android.graphics.Typeface; import java.util.HashMap; /** * Created by Rey on 12/23/2014. */ public class TypefaceUtil { private static final HashMap<String, Typeface> sCachedFonts = new HashMap<String, Typeface>(); private static final String PREFIX_ASSET = "asset:"; private TypefaceUtil() { } /** * @param familyName if start with 'asset:' prefix, then load font from asset folder. * @return */ public static Typeface load(Context context, String familyName, int style) { if(familyName != null && familyName.startsWith(PREFIX_ASSET)) synchronized (sCachedFonts) { try { if (!sCachedFonts.containsKey(familyName)) { final Typeface typeface = Typeface.createFromAsset(context.getAssets(), familyName); sCachedFonts.put(familyName, typeface); return typeface; } } catch (Exception e) { return Typeface.DEFAULT; } return sCachedFonts.get(familyName); } return Typeface.create(familyName, style); } }
ViewUtil.java
package com.rey.material.util; import java.util.concurrent.atomic.AtomicInteger; import android.annotation.SuppressLint; public class ViewUtil { public static final long FRAME_DURATION = 1000 / 60; private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); @SuppressLint("NewApi") public static int generateViewId() { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { for (;;) { final int result = sNextGeneratedId.get(); // aapt-generated IDs have the high byte nonzero; clamp to the range under that. int newValue = result + 1; if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. if (sNextGeneratedId.compareAndSet(result, newValue)) return result; } } else return android.view.View.generateViewId(); } public static boolean hasState(int[] states, int state){ if(states == null) return false; for (int state1 : states) if (state1 == state) return true; return false; } }
ListPopupWindow.java
/* * 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 java.lang.reflect.Method; import java.util.Locale; 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.example.spinnersformaterialdesign.R; /** * Static library support version of the framework's {@link android.widget.ListPopupWindow}. * Used to write apps that run on platforms prior to Android L. When running * on Android L or above, this implementation is still used; it does not try * to switch to the framework's implementation. See the framework SDK * documentation for a class overview. * * @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 = 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(android.view.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(android.view.View) */ public static final int POSITION_PROMPT_BELOW = 1; /** * Alias for {@link android.view.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 android.view.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(android.graphics.drawable.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(android.graphics.drawable.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(android.graphics.drawable.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(android.graphics.drawable.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. * * <p>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 com.rey.material.widget.ListView#setOnItemClickListener(android.widget.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 com.rey.material.widget.ListView#setOnItemSelectedListener(android.widget.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}. * * <p>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.</p> * * @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 android.view.View#onKeyPreIme(int, android.view.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 android.view.View.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}. * <p> * 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. * <p> * Example usage: * <pre> * ListPopupWindow myPopup = new ListPopupWindow(context); * myPopup.setAnchor(myAnchor); * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); * myAnchor.setOnTouchListener(dragListener); * </pre> * * @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; } }; } /** * <p>Builds the popup window's content and returns the height the popup * should have. Returns -1 when the content already exists.</p> * * @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(); } // 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*/); 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. * <p> * 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. <p> 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. <p> 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(); } } } /** * <p>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.</p> */ 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; /** * <p>Creates a new list view wrapper.</p> * * @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(); } /** * <p>Returns the focus state in the drop down.</p> * * @return true always if hijacking focus */ @Override public boolean hasWindowFocus() { return mHijackFocus || super.hasWindowFocus(); } /** * <p>Returns the focus state in the drop down.</p> * * @return true always if hijacking focus */ @Override public boolean isFocused() { return mHijackFocus || super.isFocused(); } /** * <p>Returns the focus state in the drop down.</p> * * @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."); } } } }
ListView.java
package com.rey.material.widget; import android.content.Context; import android.support.v7.internal.widget.ListViewCompat; import android.util.AttributeSet; import android.view.View; public class ListView extends ListViewCompat { private RecyclerListener mRecyclerListener; 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); } private 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); } }); } @Override public void setRecyclerListener(RecyclerListener listener) { mRecyclerListener = listener; } }
PopupWindow.java
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.example.spinnersformaterialdesign.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); } }
RippleManager.java
package com.rey.material.widget; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import com.example.spinnersformaterialdesign.R; import com.rey.material.drawable.RippleDrawable; import com.rey.material.drawable.ToolbarRippleDrawable; public final class RippleManager implements View.OnClickListener, Runnable{ private View.OnClickListener mClickListener; private View mView; public RippleManager(){} @SuppressWarnings("deprecation") @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public void onCreate(View v, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ if(v.isInEditMode()) return; mView = v; TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleView, defStyleAttr, defStyleRes); int rippleStyle = a.getResourceId(R.styleable.RippleView_rd_style, 0); RippleDrawable drawable = null; if(rippleStyle != 0) drawable = new RippleDrawable.Builder(context, rippleStyle).backgroundDrawable(mView.getBackground()).build(); else{ boolean rippleEnable = a.getBoolean(R.styleable.RippleView_rd_enable, false); if(rippleEnable) drawable = new RippleDrawable.Builder(context, attrs, defStyleAttr, defStyleRes).backgroundDrawable(mView.getBackground()).build(); } a.recycle(); if(drawable != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) mView.setBackground(drawable); else mView.setBackgroundDrawable(drawable); } } public boolean isDelayClick(){ Drawable background = mView.getBackground(); if(background instanceof RippleDrawable) return ((RippleDrawable)background).isDelayClick(); else if(background instanceof ToolbarRippleDrawable) return ((ToolbarRippleDrawable)background).isDelayClick(); return false; } public void setDelayClick(boolean delay){ Drawable background = mView.getBackground(); if(background instanceof RippleDrawable) ((RippleDrawable)background).setDelayClick(delay); else if(background instanceof ToolbarRippleDrawable) ((ToolbarRippleDrawable)background).setDelayClick(delay); } public void setOnClickListener(View.OnClickListener l) { mClickListener = l; } public boolean onTouchEvent(MotionEvent event){ Drawable background = mView.getBackground(); return background instanceof RippleDrawable && ((RippleDrawable) background).onTouch(mView, event); } @Override public void onClick(View v) { Drawable background = mView.getBackground(); long delay = 0; if(background instanceof RippleDrawable) delay = ((RippleDrawable)background).getClickDelayTime(); else if(background instanceof ToolbarRippleDrawable) delay = ((ToolbarRippleDrawable)background).getClickDelayTime(); if(delay > 0 && mView.getHandler() != null) mView.getHandler().postDelayed(this, delay); else run(); } @Override public void run() { if(mClickListener != null) mClickListener.onClick(mView); } public static void cancelRipple(View v){ Drawable background = v.getBackground(); if(background instanceof RippleDrawable) ((RippleDrawable)background).cancel(); else if(background instanceof ToolbarRippleDrawable) ((ToolbarRippleDrawable)background).cancel(); if(v instanceof ViewGroup){ ViewGroup vg = (ViewGroup)v; for(int i = 0, count = vg.getChildCount(); i < count; i++) RippleManager.cancelRipple(vg.getChildAt(i)); } } }
Spinner.java
package com.rey.material.widget; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; 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.support.v4.view.GravityCompat; import android.support.v7.internal.widget.TintManager; import android.support.v7.internal.widget.TintTypedArray; import android.support.v7.internal.widget.ViewUtils; 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.FrameLayout; import android.widget.ListAdapter; import android.widget.PopupWindow; import android.widget.SpinnerAdapter; import android.widget.TextView; import com.example.spinnersformaterialdesign.R; import com.rey.material.drawable.ArrowDrawable; import com.rey.material.drawable.DividerDrawable; import com.rey.material.drawable.RippleDrawable; import com.rey.material.util.ThemeUtil; public class Spinner extends FrameLayout { private static final int MAX_ITEMS_MEASURED = 15; private static final int INVALID_POSITION = -1; public interface OnItemClickListener{ boolean onItemClick(Spinner parent, View view, int position, long id); } public interface OnItemSelectedListener{ void onItemSelected(Spinner parent, View view, int position, long id); } private boolean mLabelEnable; private LabelView 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 = INVALID_POSITION; private RecycleBin mRecycler = new RecycleBin(); private Rect mTempRect = new Rect(); private DropDownAdapter mTempAdapter; private SpinnerDataSetObserver mDataSetObserver = new SpinnerDataSetObserver(); private TintManager mTintManager; private RippleManager mRippleManager = new RippleManager(); public Spinner(Context context) { super(context); init(context, null, R.attr.listPopupWindowStyle, 0); } public Spinner(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, R.attr.listPopupWindowStyle, 0); } public Spinner(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, 0); } public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, defStyleRes); } public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { setWillNotDraw(false); applyStyle(context, attrs, defStyleAttr, defStyleRes); if(isInEditMode()){ TextView tv = new TextView(context, attrs, defStyleAttr); tv.setText("Item 1"); super.addView(tv); } setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { showPopup(); } }); } public void applyStyle(int resId){ applyStyle(getContext(), null, 0, resId); } private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ CharSequence memoLabel = mLabelView == null ? null : mLabelView.getText(); removeAllViews(); mRippleManager.onCreate(this, context, attrs, defStyleAttr, defStyleRes); TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.Spinner, defStyleAttr, defStyleRes); mLabelEnable = a.getBoolean(R.styleable.Spinner_spn_labelEnable, false); if(mLabelEnable){ mLabelView = new LabelView(context); mLabelView.setGravity(GravityCompat.START); mLabelView.setSingleLine(true); int labelPadding = a.getDimensionPixelOffset(R.styleable.Spinner_spn_labelPadding, 0); int labelTextSize = a.getDimensionPixelSize(R.styleable.Spinner_spn_labelTextSize, 0); ColorStateList labelTextColor = a.getColorStateList(R.styleable.Spinner_spn_labelTextColor); int labelTextAppearance = a.getResourceId(R.styleable.Spinner_spn_labelTextAppearance, 0); int labelEllipsize = a.getInteger(R.styleable.Spinner_spn_labelEllipsize, 0); CharSequence label = ThemeUtil.getString(a, R.styleable.Spinner_spn_label, memoLabel); mLabelView.setText(label); mLabelView.setPadding(0, 0, 0, labelPadding); if(labelTextAppearance > 0) mLabelView.setTextAppearance(context, labelTextAppearance); if(labelTextSize > 0) mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize); if(labelTextColor != null) mLabelView.setTextColor(labelTextColor); switch (labelEllipsize) { case 1: mLabelView.setEllipsize(TextUtils.TruncateAt.START); break; case 2: mLabelView.setEllipsize(TextUtils.TruncateAt.MIDDLE); break; case 3: mLabelView.setEllipsize(TextUtils.TruncateAt.END); break; case 4: mLabelView.setEllipsize(TextUtils.TruncateAt.MARQUEE); break; default: mLabelView.setEllipsize(TextUtils.TruncateAt.END); break; } addView(mLabelView, 0, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } mGravity = a.getInt(R.styleable.Spinner_android_gravity, Gravity.CENTER); setMinimumWidth(a.getDimensionPixelOffset(R.styleable.Spinner_android_minWidth, 0)); setMinimumHeight(a.getDimensionPixelOffset(R.styleable.Spinner_android_minHeight, 0)); mPopup = new DropdownPopup(context, attrs, defStyleAttr, defStyleRes); mPopup.setModal(true); mDropDownWidth = a.getLayoutDimension(R.styleable.Spinner_android_dropDownWidth, LayoutParams.WRAP_CONTENT); mPopup.setBackgroundDrawable(a.getDrawable(R.styleable.Spinner_android_popupBackground)); mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt)); mPopup.setItemAnimation(a.getResourceId(R.styleable.Spinner_spn_popupItemAnimation, 0)); mPopup.setItemAnimationOffset(a.getInteger(R.styleable.Spinner_spn_popupItemAnimOffset, 50)); mDisableChildrenWhenDisabled = a.getBoolean(R.styleable.Spinner_disableChildrenWhenDisabled, false); mArrowAnimSwitchMode = a.getBoolean(R.styleable.Spinner_spn_arrowSwitchMode, false); int arrowAnimDuration = a.getInteger(R.styleable.Spinner_spn_arrowAnimDuration, 0); mArrowSize = a.getDimensionPixelSize(R.styleable.Spinner_spn_arrowSize, ThemeUtil.dpToPx(getContext(), 4)); mArrowPadding = a.getDimensionPixelSize(R.styleable.Spinner_spn_arrowPadding, ThemeUtil.dpToPx(getContext(), 4)); ColorStateList arrowColor = a.getColorStateList(R.styleable.Spinner_spn_arrowColor); if(arrowColor == null) arrowColor = ColorStateList.valueOf(ThemeUtil.colorControlNormal(context, 0xFF000000)); int resId = a.getResourceId(R.styleable.Spinner_spn_arrowInterpolator, 0); Interpolator arrowInterpolator = resId != 0 ? AnimationUtils.loadInterpolator(context, resId) : null; boolean arrowClockwise = a.getBoolean(R.styleable.Spinner_spn_arrowAnimClockwise, true); mArrowDrawable = new ArrowDrawable(ArrowDrawable.MODE_DOWN, mArrowSize, arrowColor, arrowAnimDuration, arrowInterpolator, arrowClockwise); mArrowDrawable.setCallback(this); mDividerHeight = a.getDimensionPixelOffset(R.styleable.Spinner_spn_dividerHeight, 0); mDividerPadding = a.getDimensionPixelOffset(R.styleable.Spinner_spn_dividerPadding, 0); int dividerAnimDuration = a.getInteger(R.styleable.Spinner_spn_dividerAnimDuration, 0); ColorStateList dividerColor = a.getColorStateList(R.styleable.Spinner_spn_dividerColor); 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); } if(mDividerHeight > 0){ mDividerDrawable = new DividerDrawable(mDividerHeight, dividerColor, dividerAnimDuration); mDividerDrawable.setCallback(this); } mTintManager = a.getTintManager(); a.recycle(); if (mTempAdapter != null) { mPopup.setAdapter(mTempAdapter); mTempAdapter = null; } if(mAdapter != null) setAdapter(mAdapter); } public View getSelectedView() { View v = getChildAt(getChildCount() - 1); return v == mLabelView ? null : v; } public void setSelection(int position) { if(mAdapter != null) position = 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(); } } public int getSelectedItemPosition(){ return mSelectedPosition; } public SpinnerAdapter getAdapter() { return mAdapter; } 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); } public void setPopupBackgroundDrawable(Drawable background) { mPopup.setBackgroundDrawable(background); } public void setPopupBackgroundResource(int resId) { setPopupBackgroundDrawable(mTintManager.getDrawable(resId)); } public Drawable getPopupBackground() { return mPopup.getBackground(); } public void setDropDownVerticalOffset(int pixels) { mPopup.setVerticalOffset(pixels); } public int getDropDownVerticalOffset() { return mPopup.getVerticalOffset(); } public void setDropDownHorizontalOffset(int pixels) { mPopup.setHorizontalOffset(pixels); } public int getDropDownHorizontalOffset() { return mPopup.getHorizontalOffset(); } public void setDropDownWidth(int pixels) { mDropDownWidth = pixels; } 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); } 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(); } @Override public void setBackgroundDrawable(Drawable drawable) { Drawable background = getBackground(); if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) ((RippleDrawable) background).setBackgroundDrawable(drawable); else super.setBackgroundDrawable(drawable); } @Override public void setOnClickListener(OnClickListener l) { if(l == mRippleManager) super.setOnClickListener(l); else{ mRippleManager.setOnClickListener(l); setOnClickListener(mRippleManager); } } public void setOnItemClickListener(OnItemClickListener l) { mOnItemClickListener = l; } public void setOnItemSelectedListener(OnItemSelectedListener l) { mOnItemSelectedListener = l; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return true; } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { boolean result = super.onTouchEvent(event); return mRippleManager.onTouchEvent(event) || result; } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || mArrowDrawable == who || mDividerDrawable == who; } private int getArrowDrawableWidth(){ return mArrowSize + mArrowPadding * 2; } 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.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); width -= paddingHorizontal; height -= labelHeight + paddingVertical; if(v != null && (v.getMeasuredWidth() != width || v.getMeasuredHeight() != height)) v.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = getPaddingLeft(); int childRight = r - l - getPaddingRight() - getArrowDrawableWidth(); int childTop = getPaddingTop(); int childBottom = b - t - getPaddingBottom(); if(mLabelView != null){ 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; 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 protected void onSizeChanged(int w, int h, int oldw, int oldh) { mArrowDrawable.setBounds(w - getArrowDrawableWidth() - getPaddingRight(), getPaddingTop() + (mLabelView == null ? 0 : mLabelView.getMeasuredHeight()), w - getPaddingRight(), h - getDividerDrawableHeight() - getPaddingBottom()); if(mDividerDrawable != null) mDividerDrawable.setBounds(getPaddingLeft(), h - mDividerHeight - getPaddingBottom(), w - getPaddingRight(), h - getPaddingBottom()); } @Override public void draw(@NonNull Canvas canvas) { super.draw(canvas); mArrowDrawable.draw(canvas); if(mDividerDrawable != null) mDividerDrawable.draw(canvas); } @Override protected void drawableStateChanged() { super.drawableStateChanged(); 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); 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(mArrowAnimSwitchMode) mArrowDrawable.setMode(ArrowDrawable.MODE_UP, true); } } private void onPopupDismissed(){ 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<SavedState> CREATOR = new Creator<SavedState>() { 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<View> 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<View> scrapHeap = mScrapHeap; scrapHeap.clear(); } } private static class DropDownAdapter implements ListAdapter, SpinnerAdapter, OnClickListener { private SpinnerAdapter mAdapter; private ListAdapter mListAdapter; private AdapterView.OnItemClickListener mOnItemClickListener; /** * <p>Creates a new ListAdapter wrapper for the specified adapter.</p> * * @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 LabelView extends android.widget.TextView{ public LabelView(Context context) { super(context); } @Override protected int[] onCreateDrawableState(int extraSpace) { return Spinner.this.getDrawableState(); } } 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 = ViewUtils.isLayoutRtl(Spinner.this) ? 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 (ViewUtils.isLayoutRtl(Spinner.this)) 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); } } }
TextView.java
package com.rey.material.widget; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import com.rey.material.drawable.RippleDrawable; public class TextView extends android.widget.TextView { private RippleManager mRippleManager = new RippleManager(); public interface OnSelectionChangedListener{ public 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); } public TextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, defStyleRes); } private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ applyStyle(context, attrs, defStyleAttr, defStyleRes); } public void applyStyle(int resId){ applyStyle(getContext(), null, 0, resId); } private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ mRippleManager.onCreate(this, context, attrs, defStyleAttr, defStyleRes); } @Override public void setBackgroundDrawable(Drawable drawable) { Drawable background = getBackground(); if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable)) ((RippleDrawable) background).setBackgroundDrawable(drawable); else super.setBackgroundDrawable(drawable); } @Override public void setOnClickListener(OnClickListener l) { if(l == mRippleManager) super.setOnClickListener(l); else{ mRippleManager.setOnClickListener(l); setOnClickListener(mRippleManager); } } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { boolean result = super.onTouchEvent(event); return mRippleManager.onTouchEvent(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); } }
—> Run Your Code.
Leave a Reply