我相信对 Drawable 有一定了解的都知道 StateListDrawable,也知道 selector 这个标签,不了解的朋友可以看我的博客Android–各种Drawable介绍。
拿Button为例,通过这个 StateListDrawable,我们可以对Button的各个状态设置不同的效果,如normal,pressed,focused或者其它。这个就是介绍 selector标签 和 Drawable State的各个状态的效果,还有如何自定义一个 Drawable State。
1、selector
1、属性
android:state_activated 当一个视图或其父母被设置为“activated”(激活),表示用户已标记为感兴趣。表示的state_checked状态应该传播到视图层次。。可通过代码调用控件的setActivated(boolean)方法设置是否激活该控件。
android:state_checked 设置是否勾选状态,主要用于CheckBox和RadioButton,true表示已被勾选,false表示未被勾选
android:state_checkable 设置勾选是否可用状态,类似state_enabled,只是state_enabled会影响触摸或点击事件,而state_checkable影响勾选事件
android:state_enabled 设置触摸或点击事件是否可用状态,一般只在false时设置该属性,表示不可用状态
android:state_focused 设置是否获得焦点状态,true表示获得焦点,默认为false,表示未获得焦点。比如用户选择了一个文本框。
android:state_hovered 设置是否鼠标在上面滑动的状态,true表示鼠标在上面滑动,默认为false,通常与focused state相同。
android:state_pressed 设置是否按压状态,一般在true时设置该属性,表示已按压状态,默认为false,如一个按钮触摸或者点击。
android:state_selected 设置是否选中状态,true表示已选中,false表示未选中。它与focus state并不完全一样,如一个list view 被选中的时候,它里面的各个子组件可能通过方向键,被选中了。
android:state_window_focused 设置当前窗口是否获得焦点状态,true表示获得焦点,false表示未获得焦点,例如拉下通知栏或弹出对话框时,当前界面就会失去焦点;另外,ListView的 item 获得焦点时也会触发true状态,可以理解为当前窗口就是 item 本身。
除了状态属性,另外selector标签下有两个比较有用的属性要说一下,添加了下面两个属性之后,则会在状态改变时出现淡入淡出效果:
- android:enterFadeDuration 状态改变时,新状态展示时的淡入时间,以毫秒为单位
- android:exitFadeDuration 状态改变时,旧状态消失时的淡出时间,以毫秒为单位
2、使用注意
selector作为drawable资源时,item指定android:drawable属性,并放于drawable目录下
selector作为color资源时,item指定android:color属性,并放于color目录下
color资源也可以放于drawable目录,引用时则用@drawable来引用,但不推荐这么做,drawable资源和color资源最好还是分开
android:drawable属性除了引用@drawable资源,也可以引用@color颜色值;但android:color只能引用@color
item是从上往下匹配的,如果匹配到一个item那它就将采用这个item,而不是采用最佳匹配的规则;所以设置默认的状态,一定要写在最后,如果写在前面,则后面所有的item都不会起作用了。
3、ListView 中的selector
最后,关于ListView的 item 样式,有两种设置方式:一种是在ListView标签里设置android:listSelector属性;另一种是在item的布局layout里设置android:background。但是,这两种设置的结果却有着不同。同时,使用ListView时也有些其他需要注意的地方,总结如下:
android:listSelector设置的 item 默认背景是透明的,不管你在selector里怎么设置都无法改变它的背景。所以,如果想改 item 的默认背景,只能通过第二种方式,在item的布局layout里设置 android:background。
当触摸点击 item时,第一种设置方式下,state_pressed、state_focused和state_window_focused设为true时都会触发,而第二种设置方式下,只有state_pressed会触发。
当item里有Button或CheckBox之类的控件时,会抢占item本身的焦点,导致item本身的触摸点击事件会无效。那么,要解决此问题,有三种解决方案:
将Button或CheckBox换成TextView或ImageView之类的控件
设置Button或CheckBox之类的控件设置focusable属性为false
设置 item 的根布局属性android:descendantFocusability=”blocksDescendants”第三种是最方便,也是推荐的方式,它会将 item 根布局下的所有子控件都设置为不能获取焦点。android:descendantFocusability属性的值有三种,其中,ViewGroup是指设置该属性的View,本例中就是item的根布局:
- beforeDescendants:ViewGroup会优先其子类控件而获取到焦点
- afterDescendants:ViewGroup只有当其子类控件不需要获取焦点时才获取焦点
- blocksDescendants:ViewGroup会覆盖子类控件而直接获得焦点
2、Drawable State
1、改变 state
我们在 xml 中确定好了 Drawable State,那么 View,ViewGroup该如何去改变 Drawable State呢,另外,我们自定义的Drawable又该如何获得 Drawable State呢。
View 的状态变化
首先看和 Drawable State 相关的几个方法:
public final int[] getDrawableState()
返回一个资源 id 的数组,这些 id 代表着 View 的当前状态。
View中的实现: 如果没有 PFLAG_DRAWABLE_STATE_DIRTY 标志,直接返回缓存的 DrawableState;否则,调用 onCreateDrawableState 获取新的状态返回,并把 PFLAG_DRAWABLE_STATE_DIRTY 标志去掉。
ViewGroup中的实现: 没有重载。protected void drawableStateChanged()
这个方法会在 View 的状态改变并影响到正在显示的 drawable 的状态的时候会被调用。如果这个 View 拥有一个 StateListAnimator 的话,这个 StateListAnimator也会被调用来执行必要的状态改变动画。如果重写这个方法的话要确保调用父类的方法。View 中的实现: 调用 getDrawableState() 获得当前的 drawable state,并把它赋值给 background 和 stateListAnimator。
ViewGroup 中的实现: 如果有 FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE 标志的话,还会遍历每个子 View,如果子 View 有 DUPLICATE_PARENT_STATE 标志,就调用子 View 的 refreshDrawableState() 方法。
protected int[] onCreateDrawableState(int extraSpace)
为 View 生成新的 Drawable 的状态。这个方法会在缓存的 Drawable state 被认为失效(invalid)之后被 view 系统所调用。如果要获取当前的状态,你应该使用 getDrawableState 方法。方法参数 extraSpace 如果不是0的话,这个值可以代表你希望返回的数组中除了装载当前的状态之外,额外的空间,你可以使用这些空间来存放你自己的状态。
View 中的实现: 如果 View 被设置为和父 View 的 drawable state 一致,返回父 view 的 drawable state。否则从各种 Flag 中获取 view 的状态信息并封装到一个数组中返回(数组长度是收集到的信息数量加上参数 extraSpace 的大小)。
ViewGroup 中的实现: 如果没有 FLAG_ADD_STATES_FROM_CHILDREN 标志的话直接调用父方法返回。否则除了调用父方法,还要遍历子 View,通过 getDrawableState 拿到它们的 drawable state,使用 mergeDrawableStates 合并到自己的 drawable state。protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState)
将你存储在 additionalState 中的状态和 baseState 中的状态合并到一起(baseState 通常是由 getDrawableState 方法得到的),为了简化,baseState 会作为参数被返回,也就是,baseArray 中必须提前预留存放 additionalState 的空间,否则也无法合并成功。
View中的实现: 从 baseState 数组第一个为 0 的元素开始,将 additionalState 数组中的内容拷贝过来,最后把 baseState 返回。
ViewGroup中的实现: 没有重载。public void refreshDrawableState()
这个方法被调用来强制更新一个 View 的 drawable state。这个方法会导致 View 的 drawableStateChanged 方法被调用。如果对新的 drawable state 感兴趣,可以通过 getDrawableState 方法获取。
View中的实现: 设置上 PFLAG_DRAWABLE_STATE_DIRTY 标志,调用 drawableStateChanged(),如果有父 View,调用父 View 的 childDrawableStateChanged() 方法。
ViewGroup中的实现: 没有重载。public void childDrawableStateChanged(View child)
这个是 ViewGroup 的方法。如果有 FLAG_ADD_STATES_FROM_CHILDREN 标志的话,刷新这个 ViewGroup 的 drawable state,将子 View 的状态加入进去.
View中的实现: 没有定义。
ViewGroup中的实现: 如果有FLAG_ADD_STATES_FROM_CHILDREN 标志,调用 refreshDrawableState() 方法。
调用流程:
- 当 View 的状态(onCreateDrawableState方法需要收集的各种标志)发生变化的时候,会调用 refreshDrawableState 方法。
- 在 refreshDrawableState 方法中会设置上 PFLAG_DRAWABLE_STATE_DIRTY 标志,然后调用drawableStateChanged 方法。
- 在 drawableStateChanged 方法中,会调用 getDrawableState 方法获取当前状态,并把状态赋值给background 和 StateListAnimator。
- 在 getDrawableState 方法中,会发现存在 PFLAG_DRAWABLE_STATE_DIRTY 标志,缓存的drawable state 已经失效,就会依次查看所有与 drawable state 相关的标志,组装新的 drawable state 返回。
自定义 Drawable 状态的变化
你在编程中可能会遇到这样的问题,当你自定义一个控件,而这个控件中除了有背景还可能会有其他的图案,你希望当你按在控件上的时候,所有的图案都发生状态的变化,但结果是只有背景发生了状态的改变。其他图案没有响应状态的变化,因为这些 drawable 没有被设置相应的状态。
想要让自己的 drawable 也像 background 一样随着控件的状态变化而变化,就需要把上面那些方法中用到 background 的位置也加上我们自己的 drawable 的操作,也就是 drawableStateChanged() 方法:
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
// mDrawalbe 是我们自己的 drawable 对象,如果有更多,需要每个都进行这样的操作
Drawable d = mDrawable;
if (d != null && d.isStateful()) {
d.setState(getDrawableState());
}
}
查看文档可以发现 ImageView 也是这样实现的。
2、自定义 Drawable State
既然是自定义,那当然要写一个例子。我们要写得类似于邮箱,邮件以ListView形式展示,但是我们需要一个状态去标识出未读和已读,我们自定义一个状态state_message_readed,判断为已读。
1、res/values/新建一个xml文件:attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MessageState">
<attr name="state_message_readed" format="boolean"/>
</declare-styleable>
</resources>
2、写Item的容器
我们这里 item 选择用RelativeLayout实现,然后根据上面我讲得 Drawable State 调用的方法,应该复写它的onCreateDrawableState方法,把我们自定义的状态在合适的时候添加进去。
public class MessageListItem extends RelativeLayout {
private static final int[] STATE_MESSAGE_READED = { R.attr.state_message_readed };
private boolean mMessgeReaded = false;
public MessageListItem(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public void setMessageReaded(boolean readed)
{
if (this.mMessgeReaded != readed)
{
mMessgeReaded = readed;
refreshDrawableState();
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace)
{
if (mMessgeReaded)
{
final int[] drawableState = super
.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(drawableState, STATE_MESSAGE_READED);
return drawableState;
}
return super.onCreateDrawableState(extraSpace);
}
}
可以看到上面的代码中出现了几个熟悉的方法,refreshDrawableState(),onCreateDrawableState() 和 mergeDrawableStates(),这和我们所说的流程吻合,更新状态调用了 onCreateDrawableState(),重写该方法添加我们的状态,逻辑很清晰。
3、布局文件 和 Drawable
colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="message_list_item_background_unread">#ff274172</color>
</resources>
message_item_bg.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:example="http://schemas.android.com/apk/res/com.ht.drawablestate"
>
<item android:state_pressed="true"
android:drawable="@android:color/transparent"
/>
<item android:state_focused="true"
android:drawable="@android:color/transparent"
/>
<item example:state_message_readed="true"
android:drawable="@color/message_list_item_background_unread"
/>
</selector>
message_item_icon_bg:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:example="http://schemas.android.com/apk/res/com.ht.drawablestate"
android:constantSize="true"
>
<item example:state_message_readed="true"
android:drawable="@drawable/message_read_status_read"
/>
<item android:drawable="@drawable/message_read_status_unread" />
</selector>
message_list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<com.ht.drawablestate.MessageListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/message_item_bg" >
<ImageView
android:id="@+id/id_msg_item_icon"
android:layout_width="30dp"
android:src="@drawable/message_item_icon_bg"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
/>
<TextView
android:id="@+id/id_msg_item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/id_msg_item_icon" />
</com.ht.drawablestate.MessageListItem>
这就是将我们写得 DrawableState 加进我们的 Drawable资源里再添加到布局文件中。
4、Activity
public class CustomActivity extends ListActivity {
private Message[] messages = new Message[] {
new Message("Gas bill overdue", true),
new Message("Congratulations, you've won!", true),
new Message("I love you!", false),
new Message("Please reply!", false),
new Message("You ignoring me?", false),
new Message("Not heard from you", false),
new Message("Electricity bill", true),
new Message("Gas bill", true),
new Message("Holiday plans", false),
new Message("Marketing stuff", false), };
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getListView().setAdapter(new ArrayAdapter<Message>(this, -1, messages)
{
private LayoutInflater mInflater = LayoutInflater
.from(getContext());
@Override
public View getView(final int position, View convertView, ViewGroup parent)
{
ViewHolder viewHolder;
if (convertView == null)
{
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.message_list_item,
parent, false);
viewHolder.mImageView = (ImageView) convertView
.findViewById(R.id.id_msg_item_icon);
viewHolder.mTextView = (TextView) convertView
.findViewById(R.id.id_msg_item_text);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
MessageListItem messageListItem = (MessageListItem) convertView;
viewHolder.mImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (getItem(position).readed == false) {
getItem(position).readed = true;
notifyDataSetChanged();
}
}
});
viewHolder.mTextView.setText(getItem(position).message);
messageListItem.setMessageReaded(getItem(position).readed);
return convertView;
}
class ViewHolder {
public TextView mTextView;
public ImageView mImageView;
}
});
}
private static class Message {
private String message;
private boolean readed;
private Message(String message, boolean readed) {
this.message = message;
this.readed = readed;
}
}
}
这个 Activity 我继承了 ListActivity,就想当于一个 ListView 啦,在getView() 中我设置了 ImageView 的监听事件,代码很简单就不解释啦。
这个程序的参考是来自于 Example for GitHub,图片资源也在里面。
结束语:本文仅用来学习记录,参考查阅。

本文详细介绍了Android中DrawableState的概念及其应用,包括StateListDrawable的使用方法、属性解析及自定义DrawableState的具体步骤。
316

被折叠的 条评论
为什么被折叠?



