本篇文章主要讲一下自己在自定义控件学习过程中所遇见的问题和自己写过一段时间后的理解,做一下记录,以便后面回溯查看。
参考链接:http://blog.csdn.net/zq2114522/article/details/53312530
1、为什么要用自定义控件
自定义控件的使用往往是出于系统原生控件并不能满足与当前需求时候,针对不同情况的处理,所以针对于自定义控件的处理,首先是先要明确目的以及需求,确定功能再针对性的进行编写
2、怎么解决自定义过程中遇见的困难
①询问同事,技术大牛
②通用的google 百度 csdn,当然里面答案也是鱼目混珠,需要不断实践才能确定答案
③查看源码,为什么我会按照这个排序,其实在项目开发过程中,我感觉时间是第一要素,能在最短时间内解决问题才是好的方法,查看源码效果是最好的,记忆效果号,但是时间耗费会相对较长所以放在第三位
3、自定义控件难点
自定义控件我认为的难点是算法,每一个好的特效都有一种好的算法,越复杂要求越高,所以努力提高自己的算法水平也是一方面。
4、自定义控件
一、属性定义
constructor: 构造方法 view自定义控件有4个参数,
context :上下文,
AttributeSet :自定义属性的调用,
defStyleAttr :默认的style,
defStyleRes :当defStyleAttr(即View的构造函数的第三个参数)不为0且在Theme中有为这个attr赋值时,defStyleRes(通过obtainStyledAttributes的第四个参数指定)不起作用
第三和第四参数目前没有套太多了解,经常使用的第二个参数,作用就是可以获取到自定义属性和xml里面数值进行关联,具体的属性值如下:
boolean
boolean表示attr是布尔类型的值,取值只能是true或false。
string
string表示attr是字符串类型。
integer
integer表示attr是整数类型,取值只能是整数,不能是浮点数。
float
float表示attr是浮点数类型,取值只能是浮点数或整数。
fraction
fraction表示attr是百分数类型,取值只能以%结尾,例如30%、120.5%等。
color
color表示attr是颜色类型,例如#ff0000,也可以使用一个指向Color的资源,比如@android:color/background_dark,但是不能用0xffff0000这样的值。
dimension
dimension表示attr是尺寸类型,例如取值16px、16dp,也可以使用一个指向类型的资源,
reference
reference表示attr的值只能指向某一资源的ID,例如取值@id/textView。
enum
enum表示attr是枚举类型,在定义enum类型的attr时,可以将attr的format设置为enum,也可以不用设置attr的format属性,但是必须在attr节点下面添加一个或多个enum节点。取值时只能取其中一个枚举值
flag
flag表示attr是bit位标记,flag与enum有相似之处,定义了flag的attr,在设置值时,可以通过|设置多个值,而且每个值都对应一个bit位,这样通过按位或操作符|可以将多个值合成一个值,我们一般在用flag表示某个字段支持多个特性,需要注意的是,要想使用flag类型,不能在attr上设置format为flag,不要设置attr的format的属性,直接在attr节点下面添加flag节点即可。
这些属性设置在values/attrs里面:
<declare-styleable name="RoundAngleImageView">
<attr name="roundWidth" format="dimension"/>
<attr name="roundHeight" format="dimension"/>
</declare-styleable>
在xml中进行属性赋值
<com.quwanbei.haihuilai.haihuilai.Views.RoundImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY"
app:roundHeight="7dp"
app:roundWidth="7dp" />
那如何在自定义view的构造函数中调用呢
TypedArray typedArray = context.obtainStyledAttributes(attr, R.styleable.RoundAngleImageView);
mRoundHeight = typedArray.getDimensionPixelSize(R.styleable.RoundAngleImageView_roundHeight, mRoundHeight);
mRoundWidth = typedArray.getDimensionPixelSize(R.styleable.RoundAngleImageView_roundWidth, mRoundWidth);
typedArray.recycle();
这样就整个完成了一个xml和java代码的互动,这样的好处也显而易见,自定义的属性可以在构建布局的时候直接进行赋值,不需要在java中进行复杂的逻辑操作。
而关于defStyle的属性赋值在上面的参考链接中已经给出,目前我对于这块了解还没有很多,继续学习后会进行补充。
二、onMesaure
1、View的宽高是有onMeasure测量后决定的,因此当涉及到listview和gridview嵌套,滑动冲突时候就在这里解决
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getLayoutParams().height == android.view.ViewGroup.LayoutParams.WRAP_CONTENT) {
int expandSpec = android.view.View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
这样就可以解决冲突问题,另外在另一次开发中也发现了一个奇怪的问题,当gridview的高度设置为wrap_content时候,并且不让gridview进行滑动,填充图片时,会出现第一张图片错位或者重复的情况,当时查找了很多资料,很多都瞎掰,最后发现的结果是,由于高度是wrap_content,gridview要动态计算高度,所以会在onmeasure中获取position=0的高度来测量,而这个测量会影响到adapter中的getView,也就是说在测量时候会调用getView,解决办法就是在onMeasure中添加一个变量isOnMeasure,用来判断当时状态是否是进行测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure = true;
......//自己的代码逻辑
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
onMeasure = false;//绘制时候改为false
super.onLayout(changed, l, t, r, b);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
//如果是onMeasure调用的就立即返回
if (gridview.onMeasure) {
return convertView;
}
return convertView;
}
上面连个例子开发中遇见的问题,而且第一个是刚开始开发都会遇见的问题,两个问题都和onMeasure都关系,也从侧面能了解onMeasure具体的功能职责。
三、onLayout
onLayout方法是ViewGroup中子View的布局方法,主要负责子控件在父布局中位置和大小,当需要对控件宽高进行控制时候,是在这里进行。
这个例子是当子控件的总长度超过屏幕宽度,就换行显示的处理,进行的操作是通过getChildCount获取子控件个数,for循环调用getChildAt(i)进行遍历。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int flowWidth = getWidth();
int childLeft = 0;
int childTop = 0;
//遍历子控件,记录每个子view的位置
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
//跳过View.GONE的子View
if (childView.getVisibility() == View.GONE) {
continue;
}
//获取到测量的宽和高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//因为子View可能设置margin,这里要加上margin的距离
MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
if (childLeft + mlp.leftMargin + childWidth + mlp.rightMargin > flowWidth) {
//换行处理
childTop += (mlp.topMargin + childHeight + mlp.bottomMargin);
childLeft = 0;
}
//布局
int left = childLeft + mlp.leftMargin;
int top = childTop + mlp.topMargin;
int right = childLeft + mlp.leftMargin + childWidth;
int bottom = childTop + mlp.topMargin + childHeight;
childView.layout(left, top, right, bottom);
childLeft += (mlp.leftMargin + childWidth + mlp.rightMargin);
}
}
四、onDraw
这里其实就是绘制图形的方法了,自定义控件的主要绘制就在这里了,我前面有一篇自定义日历,有兴趣的可以看一下,里面的onDraw方法,就进行了日历行列的绘制,这里就不详细讲述了,有很多资料将onDraw的,可以查阅一下。
五、onTouchEvent
view的事件处理
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {}
这是官方文档注释,可以看出ontouchEvent主要是处理触摸屏幕的事件,参数是MotionEvent 事件 返回值为true说明你已经消费了此次事件,false则分发给其他;
case MotionEvent.ACTION_DOWN://手指按下
case MotionEvent.ACTION_MOVE://手指移动
case MotionEvent.ACTION_UP: //手指抬起
//使用 event.getAction();获取到action
switch(action){
case MotionEvent.ACTION_DOWN:
//处理按下操作;
...
break;
...
}
这里也有一个小知识点,怎么通过event判断是不是点击事件呢?
我们可以通过手指抬起时候的upx,upy和手指按下时候的downx,downy进行比较,当偏移量小于一个值的时候,我们判定为点击,而这个值系统中也有定义:
//获取点击事件的最大偏移量
int touchSlop =ViewConfiguration.get(context).getScaledTouchSlop();
case MotionEvent.ACTION_UP:
if (Math.abs(upX - downX) < touchSlop && Math.abs(upY - downY) < touchSlop) {
//消费点击事件
}
...
break;
当然,当我们处理完成event后,肯定会要求重新绘制新的View,这时候只需要调用一个方法就行:
//刷新 只能在主线程(UI线程)中,速度快
invalidate();
//提醒刷新 可以在工作线程(子线程)中,速度慢
postInvalidate();
记得这两个刷新的区别,分线程调用不同的方法。