Android控件点击/选择后控件背景变色的实现方式有很多种,例如使用selector的xml文件实现。这里介绍一下另一种Android原生的点击/选择实现方案(API28及以上),也就是ColorStateListDrawable
。
ColorStateListDrawable是一个可根据不同状态显示不同颜色的Drawable。
实现效果,选择前/选择后:
这里我们利用继承LinearLayoutCompat
的方式来实现:
属性
创建自定义属性:
<attr name="carbon_chipStyle" format="reference" />
<declare-styleable name="Chip">
<attr name="android:text"/>
<attr name="android:background" />
<attr name="pressed_color" format="color"/>
<attr name="checked_color" format="color"/>
<attr name="un_enable_color" format="color"/>
<attr name="carbon_icon" />
<attr name="carbon_removable" format="boolean" />
<attr name="android:checked" />
</declare-styleable>
布局
创建布局文件
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<FrameLayout
android:id="@+id/carbon_chipContent"
android:layout_width="@dimen/carbon_iconSize"
android:layout_height="@dimen/carbon_iconSize"
android:layout_margin="@dimen/carbon_chipCloseMargin"
tools:visibility="gone" />
<ImageView
android:id="@+id/carbon_chipCheck"
android:layout_width="@dimen/carbon_iconSize"
android:layout_height="@dimen/carbon_iconSize"
android:layout_margin="@dimen/carbon_chipCloseMargin"
android:visibility="gone"
android:src="@drawable/carbon_check"
tools:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/carbon_chipText"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:layout_marginHorizontal="@dimen/carbon_chipPadding"
tools:text="text" />
<ImageView
android:id="@+id/carbon_chipClose"
android:layout_width="@dimen/carbon_iconSize"
android:layout_height="@dimen/carbon_iconSize"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/carbon_chipCloseMargin"
android:scaleType="center"
android:src="@drawable/carbon_remove" />
</merge>
public class Chip extends LinearLayoutCompat implements Checkable {
/**
* Interface definition for a callback to be invoked when the checked state of a chip
* changed.
*/
public interface OnCheckedChangeListener {
/**
* Called when the checked state of a chip has changed.
*
* @param chip The chip whose state has changed.
* @param isChecked The new checked state of buttonView.
*/
void onCheckedChanged(Chip chip, boolean isChecked);
}
private FrameLayout content;
private ImageView check;
private TextView title;
private ImageView close;
private OnRemoveListener onRemoveListener;
private boolean checkedState = false;
private OnCheckedChangeListener onCheckedChangeListener;
public interface OnRemoveListener {
void onDismiss();
}
public Chip(Context context) {
super(context, null, R.attr.carbon_chipStyle);
initChip(null, R.attr.carbon_chipStyle, R.style.carbon_Chip);
}
public Chip(Context context, CharSequence text) {
super(context, null, R.attr.carbon_chipStyle);
initChip(null, R.attr.carbon_chipStyle, R.style.carbon_Chip);
setText(text);
}
public Chip(Context context, AttributeSet attrs) {
super(context, attrs, R.attr.carbon_chipStyle);
initChip(attrs, R.attr.carbon_chipStyle, R.style.carbon_Chip);
}
public Chip(Context context, AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
initChip(attrs, defStyleAttr, R.style.carbon_Chip);
}
private static int[] colorStateIds = new int[]{
R.styleable.Chip_android_background,
R.styleable.Chip_pressed_color,
R.styleable.Chip_checked_color,
R.styleable.Chip_un_enable_color
};
private void initChip(AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
inflate(getContext(), R.layout.carbon_chip, this);
title = findViewById(R.id.carbon_chipText);
content = findViewById(R.id.carbon_chipContent);
check = findViewById(R.id.carbon_chipCheck);
close = findViewById(R.id.carbon_chipClose);
close.setOnClickListener(v -> {
if (onRemoveListener != null)
onRemoveListener.onDismiss();
});
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.Chip, defStyleAttr, defStyleRes);
// 初始化背景
Carbon.initDefaultBackground(this, a, colorStateIds);
// 初始化相关自定义属性
setText(a.getString(R.styleable.Chip_android_text));
setIcon(Carbon.getDrawable(this, a, R.styleable.Chip_carbon_icon, 0));
setRemovable(a.getBoolean(R.styleable.Chip_carbon_removable, false));
a.recycle();
}
@Deprecated
public void setText(String text) {
setText((CharSequence) text);
}
public void setText(CharSequence text) {
if (text != null) {
title.setText(text);
title.setVisibility(View.VISIBLE);
} else {
title.setVisibility(View.GONE);
}
}
public void setText(int resId) {
setText(getResources().getString(resId));
}
public String getText() {
return (String) title.getText();
}
public View getTitleView() {
return title;
}
public void setIcon(int iconRes) {
content.removeAllViews();
if (iconRes == 0) {
content.setVisibility(GONE);
return;
}
content.setVisibility(VISIBLE);
ImageView icon = new ImageView(getContext());
content.addView(icon);
icon.setImageResource(iconRes);
}
public void setIcon(Drawable drawable) {
content.removeAllViews();
if (drawable == null) {
content.setVisibility(GONE);
return;
}
content.setVisibility(VISIBLE);
ImageView icon = new ImageView(getContext());
content.addView(icon);
icon.setImageDrawable(drawable);
}
public void setIcon(Bitmap bitmap) {
content.removeAllViews();
if (bitmap == null) {
content.setVisibility(GONE);
return;
}
content.setVisibility(VISIBLE);
ImageView icon = new ImageView(getContext());
content.addView(icon);
icon.setImageBitmap(bitmap);
}
@Deprecated
public Drawable getIcon() {
if (content.getChildCount() > 0 && content.getChildAt(0) instanceof ImageView)
return ((ImageView) content.getChildAt(0)).getDrawable();
return null;
}
@Deprecated
public View getIconView() {
if (content.getChildCount() > 0 && content.getChildAt(0) instanceof ImageView)
return content.getChildAt(0);
return null;
}
public View getContentView() {
if (content.getChildCount() > 0)
return content.getChildAt(0);
return null;
}
public void setContentView(View view) {
content.removeAllViews();
if (view != null) {
content.setVisibility(VISIBLE);
content.addView(view);
} else {
content.setVisibility(GONE);
}
}
public void setRemovable(boolean removable) {
close.setVisibility(removable ? VISIBLE : GONE);
}
public boolean isRemovable() {
return close.getVisibility() == VISIBLE;
}
public void setOnRemoveListener(OnRemoveListener onRemoveListener) {
this.onRemoveListener = onRemoveListener;
}
}
重点在于为控件手动设置一个ColorListDrawable充当背景图片:
// 为控件设置一个背景图片
public static void initDefaultBackground(View view, TypedArray a, int[] ids) {
Drawable d = getDefaultColorDrawable(view, a, ids);
if (d != null)
view.setBackgroundDrawable(d);
}
// 根据我们提供的android:background,pressed_color,checked_color,un_enable_color的值生成一个ColorStateListDrawable
public static Drawable getDefaultColorDrawable(View view, TypedArray a, int[] ids) {
ColorStateList color = getDefaultColorStateList(view, a, ids);
if (color != null) {
Drawable d = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
d = new ColorStateListDrawable(color);
}
return d;
}
return null;
}
public static ColorStateList getDefaultColorStateList(View view, TypedArray a, int[] ids) {
Context context = view.getContext();
int chip_bg = ids[0];
int chip_pressed_bg = ids[1];
int chip_checked_bg = ids[2];
int chip_un_enable_bg = ids[3];
if (!a.hasValue(chip_bg))
return null;
int backgroundColor = a.getColor(chip_bg, 0);
int pressedBgColor = a.getColor(chip_pressed_bg,ContextCompat.getColor(context, R.color.carbon_colorControlPressed));
int checkedBgColor = a.getColor(chip_checked_bg,ContextCompat.getColor(context,R.color.carbon_colorControlActivated));
int unEnableBgColor = a.getColor(chip_un_enable_bg,ContextCompat.getColor(context,R.color.carbon_colorControlDisabled));
return ColorStateListFactory.getInstance().make(context,backgroundColor,
pressedBgColor,
checkedBgColor,
unEnableBgColor,
getThemeColor(context,com.google.android.material.R.attr.colorError));
}
ColorStateListFactory
状态和颜色一一对应
public ColorStateList make(Context context,int defaultColor,int pressed,int activated,int disabled,int invalid){
return new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_enabled}, // unenable
new int[]{android.R.attr.state_pressed}, // pressed
new int[]{android.R.attr.state_checked}, //checked
new int[]{android.R.attr.state_activated},//activated
new int[]{android.R.attr.state_selected},//selected
new int[]{android.R.attr.state_focused},//focused
new int[]{}
},
new int[]{
disabled,
pressed,
activated,
activated,
activated,
activated,
defaultColor
});
}
这样,我们就实现了按下控件,控件的背景颜色就会改变。
但是,LinearCompact本身是没有check状态的,因此这就需要我们为它添加check状态。
Checkable接口
Chip实现Checkable接口:
public class Chip extends LinearLayoutCompat implements Checkable {
// 定义状态集
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public interface OnCheckedChangeListener {
/**
* Called when the checked state of a chip has changed.
*
* @param chip The chip whose state has changed.
* @param isChecked The new checked state of buttonView.
*/
void onCheckedChanged(Chip chip, boolean isChecked);
}
...
public void toggle() {
setChecked(!isChecked());
}
@Override
public boolean performClick() {
toggle();
if (onCheckedChangeListener != null)
onCheckedChangeListener.onCheckedChanged(this, isChecked());
final boolean handled = super.performClick();
if (!handled) {
// View only makes a sound effect if the onClickListener was
// called, so we'll need to make one here instead.
playSoundEffect(SoundEffectConstants.CLICK);
}
return handled;
}
@ViewDebug.ExportedProperty
public boolean isChecked() {
return checkedState;
}
/**
* <p>Changes the checked state of this chip.</p>
* 第二步
* 在设置状态时却没有触发到这个状态。所以我们需要自己去触发这个check状态。
* @param checked true to check the chip, false to uncheck it
*/
public void setChecked(boolean checked) {
if (this.checkedState != checked) {
checkedState = checked;
check.setVisibility(checked ? VISIBLE : GONE);
// 在状态改变时,调用refreshDrawableState()刷新状态。
refreshDrawableState();
}
}
// 第一步,我们要把状态给加进去。我们需要重写protected int[] onCreateDrawableState(int extraSpace)方法;
/**
* 先调用父类的onCreateDrawableState方法得到状态数组对象drawableState,但是参数extraSpace要加上1,因为我们要往里面增加一个状态。
* 然后判断在代码逻辑中,是否为选中状态,如果是的话,调用mergeDrawableStates(drawableState, CHECKED_STATE_SET)方法把我们的状态值给加进去,
* 最终返回drawableState。
* @param extraSpace if non-zero, this is the number of extra entries you
* would like in the returned array in which you can place your own
* states.
*
* @return
*/
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
/**
* Register a callback to be invoked when the checked state of this chip changes.
*
* @param listener the callback to call on checked state change
*/
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
onCheckedChangeListener = listener;
}
}
怎么使用
<com.chinatsp.shapebutton.chip.Chip
android:id="@+id/chip"
android:layout_width="100dp"
android:layout_height="@dimen/carbon_chipHeight"
android:layout_margin="16dp"
android:text="HELLO"
android:background="@color/carbon_defaultColorControl"
android:clickable="true"
android:checked="false"
app:checked_color="@color/carbon_red_700"
app:un_enable_color="@color/carbon_grey_700"/>
完整代码可查看:
Chip部分