用文字札记描绘自己 android学习之路
转载请保留出处 by Qiao
http://blog.csdn.net/qiaoidea/article/details/45599593
【导航】
- 多行文本折叠展开 自定义布局View实现多行文本折叠和展开
前面封装view的时候用到了自定义属性,觉得有必要单独讲一下这部分,但是呢,又不想向其他文章一样千篇一律地写这些东西。所以呢,后便会加一些临时的发散思维,引用点有意思的东西。分享东西嘛,随性点儿。
回归正题,我们想在view中使用自定义属性要怎么做呢?
其实有如下几点:
xmlns申明与引用 在你要使用的地方引入命名空间并使用这些属性,赋值
然后我们来尝试通过这些步骤做些自定义view,同时呢,我期望能方便快捷的绑定一些事件,执行相应操作。尝试来做一下。
xml直接定义view和点击事件的demo
其实自定义view的属性算是比较常见的,想来想去却没想到什么比较好写的view。就拿最常见的设置选项来说吧,我希望直接通过简单的xml配置就可以设置其字体大小颜色内容,图标和点击触发的事件。
在res/values目录下新建一个attrs.xml的文件,利用declare-styleable定义我们的属性样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RowItem"><!-- 样式名为RowItem-->
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="text" format="string" />
<attr name="textStyle"> <!-- text样式(粗体/斜体)-->
<flag name="blod" value="1"/>
<flag name="italic" value="2"/>
</attr>
<attr name="icon" format="reference"/>
<attr name="position" > <!-- 该行所处位置-->
<enum name="single" value="-1"/>
<enum name="top" value="0"/>
<enum name="middle" value="1"/>
<enum name="bottom" value="2"/>
</attr>
<attr name="action" format="string" /><!-- 执行动作-->
</declare-styleable>
</resources>
简单讲解下其中,
标签attr就是用来的定义具体的属性,name代表属性名,format代表属性的类型。
Attrs.xml文件中属性类型format值的格式
引用型reference
定义:
< attr name = “background” format = “reference” />
使用:
tools:background = “@drawable/图片ID”
颜色型color
定义:
< attr name = “textColor” format = “color” />
使用:
tools:textColor = “#ffffff”
布尔型boolean
定义:
< attr name = “focusable” format = “boolean” />
使用:tools: focusable = “true”
尺寸型dimension
定义:
< attr name = “layout_width” format = “dimension” />
使用:
tools: layout_width = “42dip”
浮点型float
定义:
< attr name = “fromAlpha” format = “float” />
使用:tools: fromAlpha = “1.0”
整型integer
定义:
< attr name = “frameDuration” format = “integer” />
使用:
tools: frameDuration = “100”
字符串string
定义:
< attr name = “apiKey” format = “string” />
使用:
tools: apiKey = “dsegergegasefwg”
百分数fraction
定义:
< attr name = “pivotX” format = “fraction” />
使用:
tools: pivotx = “200%”
枚举型enum:
定义:
< attr name=”orientation”>
< enum name=”horizontal” value=”0” />
< enum name=”vertical” value=”1” />
< /attr>
使用:
android:orientation = “vertical”
标志位、位或运算,格式如下:
定义:
< attr name=”windowSoftInputMode”>
< flag name = “stateUnspecified” value = “0” />
< flag name = “stateUnchanged” value = “1” />
< flag name = “adjustUnspecified” value = “0x00” />
< flag name = “adjustResize” value = “0x10” />
< /attr>
使用:
android:windowSoftInputMode = “stateUnspecified | stateUnchanged | stateHidden”>
属性定义可以指定多种类型:
定义:
< attr name = “background” format = “reference|color” />
使用:
android:background = “@drawable/图片ID|#00FF00”
View的默认构造方法里有 XXX(Context context, AttributeSet attrs) ,而这个 AttributeSet 参数即为属性集合,我们可以利用 TypedArray 来获取我们想要的属性。
可以看我这里自定义的View类RowItem,它获取自定义属性的方法为initWithAttrs。它通过这个方法获取最终的属性值的过程:
protected void initWithAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.RowItem);
textColor = a.getColor(R.styleable.RowItem_textColor,
defaultTextColor);
textStyle = a.getColor(R.styleable.RowItem_textStyle,
-1);
textSize = a.getDimensionPixelSize(R.styleable.RowItem_textSize, defaultTextSize);
text = a.getString(R.styleable.RowItem_text);
icon = a.getDrawable(R.styleable.RowItem_icon);
position = a.getInt(R.styleable.RowItem_position, defaultPosition);
if(icon == null){
icon = getResources().getDrawable(defaultIconId);
}
action = a.getString(R.styleable.RowItem_action);
a.recycle();
initViews();//利用属性值设置绑定View
}
先通过context.obtainStyledAttributes()方法将attrs.xml中定义的属性与AttributeSet 关联起来并映射到 TypedArray a,然后通过 a.getXXX()来获取相应属性,第二个参数为取不到时的默认值。最后用a.recycle()来回收释放。
详细讲下怎个自定义View。转回我们的自定义RowItem内部,来看一下View的实现。先定义了一个ImageVIew和TextView,然后定义对应的属性和默认值:
public class RowItem extends FrameLayout{
protected ImageView iconView;
protected TextView contentView;
/** *对应自定义属性 */
protected int textColor;
protected int textStyle;
protected float textSize;
protected String text;
protected int position;
protected Drawable icon;
protected String action;
/** *默认属性属性 */
public int defaultTextColor = Color.BLACK;
public int defaultTextSize = 12;
public int defaultPosition = -1;
private int defaultIconId = R.drawable.ic_launcher;
//构造方法
public RowItem(Context context) {
super(context);
initWithAttrs(context,null);//null则用默认属性值初始化
}
//带AttributeSet 的构造方法
public RowItem(Context context, AttributeSet attrs) {
super(context, attrs);
initWithAttrs(context,attrs);//初始化自定义属性
bindListener();
}
//...其他代码
可以看到,两个构造方法都使用了initWithAttrs()方法,设置绑定属性值,第一个构造方法中attrs为null则意味着使用默认值。在初始化了view的属性值之后,在方法最后一段,调用了initViews()来初始化view:
//利用获取的属性值来设置view
protected void initViews(){
View root = LayoutInflater.from(getContext()).inflate(R.layout.row_item, RowItem.this);
iconView = (ImageView) findViewById(R.id.item_image);
contentView = (TextView) findViewById(R.id.item_tv);
iconView.setImageDrawable(icon);
if(!TextUtils.isEmpty(text))
contentView.setText(text);
contentView.setTextSize(TypedValue.COMPLEX_UNIT_PX,textSize);
contentView.setTextColor(textColor);
contentView.setTypeface(null, textStyle);
root.setBackgroundResource(getBackGroundResource(position));
}
/** *根据当前rowItem位置来返回相应的背景(其实我们可以直接在xml中设置rowitem背景,这里只是用于演示使用枚举型常量定义的属性) */
private int getBackGroundResource(int position2) {
switch(position){
case 0:
return R.drawable.top_item_click_bg;
case 1:
return R.drawable.middle_item_click_bg;
case 2:
return R.drawable.bottom_item_click_bg;
default:
return R.drawable.single_item_click_bg;
}
}
整个自定义view的过程就讲完了。另外,细心地朋友会发现在第二个构造方法中使用了bindListener()来绑定点击事件,这里用的的是view的属性参数action,当用户点击这个view之后我们根据action来映射执行相应动作。它具体干嘛了我们后边再说,这里先继续将我们的自定义view RowItem。
使用的时候,直接在xml中引入这个view,不过在此之前,要先引入命名空间。xmlns表示xml 的 namespace。后边跟的名字可以自定,以便后便使用。这里使用demo
自动引用 使用res-auto
xmlns:demo=”http://schemas.android.com/apk/res-auto”
引用指定包名
xmlns:demo=”http://schemas.android.com/apk/res/com.qiao.demo”
然后我们就可以引入自定义属性了
<!--使用 demo开头的即为自定义属性,-->
<com.qiao.demo.RowItem android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dip" demo:position="single" <!- 其中 申明为enum枚举类可以选一-->
demo:text="测试选项"
demo:textSize="12dip"
demo:textStyle="blod|italic" <!- 而使用flag位的可以组合使用-->
demo:icon="@android:drawable/btn_star"
app:action="TestAction"/>
在activity里边就可以直接使用像使用TextView之类的空间一样使用 RowItem 了,当然也可以获取和设置相应属性了。到这里我们的自定义view也Ok了。然后呢,我们也可以直接通过setOnClickListener来设置绑定Rowitem点击事件。
但追求更简单的我们,当然期望直接指定view的点击处理方法的函数了。
前面预留了一个action属性值,当然,意味着我们想直接在xml中指定我们的自定义view点击后想要执行的操作方法了。这里呢,我们尝试使用反射来找到类里边预定义好的处理方法,而具体执行什么处理方法,在于我们action里边的关键字。
说的很抽象,不要紧,我们看那段bindLinstener()的代码。
private void bindListener() {
if(TextUtils.isEmpty(action)) return; //如果没有执行动作则返回,否则设置点击事件
setOnClickListener(new OnClickListener() {
private Method mHandler; //Method处理方法
@Override
public void onClick(View v) {
if (mHandler == null) {
try {
/** * 尝试从context中获取包含关键字为 action()参数为View的方法 * 这里的context就是添加整个自定义view的activity,所以 * 我们要获取Method方法其实就是 * 在activity中找到action(View view)这样的public方法 * 不清楚的可以查阅class的getMethod方法 */
mHandler = getContext().getClass().getMethod(action,
View.class);
} catch (NoSuchMethodException e) {
//处理异常
}
}
try {
//传入参数RowItem.this,执行这个方法
mHandler.invoke(getContext(), RowItem.this);
} catch (IllegalAccessException e) {
//处理异常
} catch (InvocationTargetException e) {
//处理异常
}
}
});
}
这里使用了java映射,有疑问的朋友自行谷歌/百度。简明扼要说下,如果action的值不为空,(比如为 clickAction),那么我们会在activity中查找 public XX clickAction(View view)这个方法,然后执行这个方法。
这个过程是,首先实例化一个类,取得方法getMethod,然后执行invoke()。
然后,在我们的activity中定义相应的public 执行方法比如
public class MainActivity extends Activity {
//其他代码
public void TestAction(View view){
Toast.makeText(getApplicationContext(), "TestAction", Toast.LENGTH_SHORT).show();
}
}
然后,我们就可以在xml的view中使用action属性设置处理点击事件的方法
demo:action=”TestAction”/>
整段代码
<com.qiao.demo.RowItem android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dip" demo:position="single" demo:text="测试选项" demo:textSize="12dip" demo:textStyle="blod|italic" demo:icon="@android:drawable/btn_star" demo:action="TestAction"/>
最后,运行测试。点击该item,弹出Toast,大功告成。
这里只举了一个例子一个方法来绑定action。当我们有很多个rowitem时候,每个item点击执行不同的动作,然后这些item又有各种变化可能,我们就不放采用这种方式,简单快速的绑定方法和事件,省去多余的findView等繁琐操作。
透露一下,其实这种用法android本身就有提供,每个view都可以在xml设置onClick参数,它对应的属性也是点击执行相应动作。
android:onClick=”TestAction”
适用于所有view,就比如TextView
<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="测试点击事件" android:onClick="TestAction"/>
然后同样在 activity重定义public void TestAction(View view)方法,运行,点击textView,是不是也同样弹出了toast?
我们来看View内部这部分实现:
case R.styleable.View_onClick:
if (context.isRestricted()) {
throw new IllegalStateException("The android:onClick attribute cannot "
+ "be used within a restricted context");
}
final String handlerName = a.getString(attr);
if (handlerName != null) {
setOnClickListener(new OnClickListener() {
private Method mHandler;
public void onClick(View v) {
if (mHandler == null) {
try {
mHandler = getContext().getClass().getMethod(handlerName,
View.class);
} catch (NoSuchMethodException e) {
int id = getId();
String idText = id == NO_ID ? "" : " with id '"
+ getContext().getResources().getResourceEntryName(
id) + "'";
throw new IllegalStateException("Could not find a method " +
handlerName + "(View) in the activity "
+ getContext().getClass() + " for onClick handler"
+ " on view " + View.this.getClass() + idText, e);
}
}
try {
mHandler.invoke(getContext(), View.this);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not execute non "
+ "public method of the activity", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException("Could not execute "
+ "method of the activity", e);
}
}
});
}
break;
哈哈,其实就是我前面讲的那部分通过自定义view属性并绑定事件。
但实际开发中,通常我们会封装好一个类,专门负责相关处理逻辑,然后给外部调用。这样是为了使业务逻辑跟界面UI层剥离开来,达到松耦合,以便于后边维护和变更。
那么,如果我期望将处理点击事件的逻辑单独包装成一个hanlderAction(处理事件类),然后在这个view中通过action参数来调用对应方法,又该怎么实现呢?
public class TestActionInvoker {
private static Map<String, Method> methodMap = new HashMap<String, Method>();
public static void tryInvoke(String method){
Method mHandler = methodMap.get(method);
if (mHandler == null) {
try {
mHandler = TestActionInvoker.class.getMethod(method);
} catch (NoSuchMethodException e) {
Log.e("NoSuchMethodException", "Could not find a method " +
method + " in the class "
+ TestActionInvoker.class + " for onClick handler"
+ " on view ");
}
}
try {
mHandler.invoke(TestActionInvoker.class);
} catch (IllegalAccessException e) {
Log.e("IllegalAccessException", "Could not execute non "
+ "public method of the class");
} catch (InvocationTargetException e) {
Log.e("InvocationTargetException", "Could not execute "
+ "method of the class");
}
}
public static void TestAction(){
Log.i("TestAction", "myInvoker");//这里只是简单的打印日志
}
}
“` java
public class ActionInvoker {
private static ActionInvoker instance;
private static Map
上边这些具体有甚用处呢,且听我后边慢慢为你道来。
通常有些页面的配置菜单会发生频繁变更,有时候是一级菜单有时候是二级菜单,而且样式不便,对应的位置和执行动作也会变化。这样就给开发带来不少麻烦。所以呢,我们可以简单的封装,然后定义一个属性对象,直接使用数组列表来初始化这么一个页面,当然前提是自己有另外封装了处理逻辑。举个伪代码小例子。
//属性对象类
ItemAttrs{
attr1;
attr2;
attr3;
}
//在界面定义个List
List<ItemAttrs> list;
//按照list产生界面
new RowItem();
rowitem.bind(lit.get(i)) //绑定到rowitem相关属性,产生界面和点击事件
//处理逻辑类
ItemAttrs{
action1();
action2();
action3();
}
这样你的界面就完全由list来控制,这个界面可以是在打包配置的一个xml里,也可以是有在线获取的json产生。你完全可以用你最方便的场景动态产生这个界面。
最后,附上前面所讲的示例源码:
点击下载示例源码