枕上诗书闲处好,门前风景雨来佳。这篇文章主要讲述Android艺术开发探索第四章——View的工作原理(下)相关的知识,希望能为你提供帮助。
android艺术开发探索第四章——View的工作原理(
下)
我们上篇BB了这么多, 这篇就多多少少要来点实战了, 上篇主席叫我多点自己的理解, 那我就多点真诚, 少点套路了, 老司机, 开车吧!
我们这一篇就扯一个内容, 那就是自定义View
- 自定义View
- 自定义View的分类
- 自定义View的须知
- 自定义View的实例
- 自定义View的思想
自定义View百花齐放, 没有什么具体的分类, 不过可以从特性大致的分为4类, 其实在我看来, 就三类, 继承原生View, 继承View和继承ViewGroup。
- 1.继承View重写onDraw方法
重写了绘制, 一般就是想自己实现某些图形了, 因为原生控件已经满足不了你了, 很显然这需要绘制的方式来完成, 采用这个方式需要自身支= warp_content,并且pading也要自己处理, 比较考验你的功底了
- 2.继承ViewGroup派生出来的Layout
这个相当于重写容器了, 当某些效果看起来像是View的组合的时候, 就是他上场的时候了, 不过这个很复杂, 需要合理的使用测量和布局这两个过程, 还要兼顾子元素的这两个过程
- 3.继承特定的View
比如TextView, 就是重写原生的View嘛, 比如你想让TextView默认有颜色之类的, 有一些小改动, 这个就可以用它的, 他相对来说比较简单, 这个就不需要自己支持包裹内容和pading了
- 4.继承特定的ViewGroup
这个和上述一样, 只不过是重写容器而已, 这个也比较常见, 事件分发的时候用的也多
这节大致的说一下注意事项
- 1.让View支持warp_content
这个在之前将测量的时候说过, 如果你不特殊处理一下是达不到满意的效果的, 这里就不重复了
- 2.如果有有必要,
让你的View支持padding
这是因为如果你不处理下的话, 那么该属性是不会生效的, 在ViewGroup也是一样
- 3.尽量不要在View中使用Handler
为什么不能用, 是因为没有必要, View本身就有一系列的post方法, 当然, 你想用也没人拦着你, 我倒是觉得handler写起来代码简洁很多
- 4.View中如果有线程或者动画,
需要及时停止,
参考View#onDetachedFromWindow
这个问题那就更好理解了, 你要是不停止这个线程或者动画, 容易导致内存溢出的, 所以你要在一个合适的机会销毁这些资源, 在Activity有生命周期, 而在View中, 当View被remove的时候, onDetachedFromWindow会被调用, , 和此方法对应的是onAttachedToWindow
- 5.View带有滑动嵌套时,
需要处理好滑动冲突
滑动冲突之前就BB过, 这里就不讲了
- 1.继承View重写onDraw方法
我们来实现一个很简单的图形: 圆。尽管如此, 还是有很多细节需要注意的, 实现的过程中需要考虑warp_content和padding, OK, 我们先来看代码
public class CircleView extends View {//颜色
private int mColor =
Color.RED;
//画笔样式
private Paint mPaint =
new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}//初始化
private void init() {
//设置颜色
mPaint.setColor(mColor);
}@
Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//View的宽
int width =
getWidth();
//View的高
int height =
getHeight();
//圆的半径 =
宽和高比较出的数 / 2
int radiu =
Math.min(width, height) / 2;
//绘制圆
canvas.drawCircle(width / 2, height / 2, radiu, mPaint);
}
}
上面的代码就绘制出了一个圆, 运行看下效果
文章图片
上面的代码很简单, 估摸着会点自定义的完全能写出来, 我们写这个案例就是要抛砖引玉, 不信, 我们接着看下去, 我们把布局改成这个样子
<
?xml version=
"
1.0"
encoding=
"
utf-8"
?>
<
LinearLayout xmlns:android=
"
http://schemas.android.com/apk/res/android"
android:layout_width=
"
match_parent"
android:layout_height=
"
match_parent"
android:orientation=
"
vertical"
>
<
com.liuguilin.viewwork.view.CircleView
android:layout_width=
"
match_parent"
android:layout_height=
"
100dp"
android:background=
"
#000000"
/>
<
/LinearLayout>
现在我们来运行一下你就会看到不一样的效果了
文章图片
接下来我们再调整一下, 只给他增加一个
android:layout_margin=
"
20dp"
这样会是什么效果呢?
文章图片
这样按理说也是我们预期的效果, 对吧, 这样的话margin属性是生效的, 这是因为margin由父容器所控制的, 所以不需要View去动, 我们进一步实验, 我现在给他继续增加, 加上一个padding
android:padding=
"
20dp"
这里是重头戏了, 我们运行后会发现, 他没什么反应呀, 我们之前说过, 如果你直接继承View,在测量的时候需要做点处理的, 不然的话, 你的warp_content就和match_parent是一样的了。
为了解决这几个问题, 我们需要做如下的处理
首先, 关于warp_content的问题, 我们只需要指定一个warp_content模式宽/高即可, 比如设置200px作为默认的宽高
其次, 针对padding的问题, 我们再绘制的时候考虑进去就好了, 修改后的onDraw如下
@
Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//padding值
int left =
getPaddingLeft();
int right =
getPaddingRight();
int top =
getPaddingTop();
int bottom =
getPaddingBottom();
//View的宽
int width =
getWidth() - left - right;
//View的高
int height =
getHeight() - top - bottom;
//圆的半径 =
宽和高比较出的数 / 2
int radiu =
Math.min(width, height) / 2;
//绘制圆
canvas.drawCircle(left +
width / 2, top +
height / 2, radiu, mPaint);
}
这样就解决了, 主要的逻辑就是绘制的时候考虑到View四周的空白即可, 圆心和半径都会考虑到, 现在我们来运行下, 就有效果了
文章图片
最后, 为了让View更加容易应用, 我们需要提供一些自定义的属性, 这些怎么玩呢, 我们继续看
第一步实在values目录下面创建自定义属性的xml, 比如attrs.xml,也可以其他名字, 名字没什么限制, 不过为了规范, 还是…你懂的, 我们就来写一个
<
?xml version=
"
1.0"
encoding=
"
utf-8"
?>
<
resources>
<
declare-styleable name=
"
CircleView"
>
<
attr name=
"
circle_color"
format=
"
color"
/>
<
/declare-styleable>
<
/resources>
这个很简单吧, 我们只定义了一个颜色的属性, 这里面有个format是类型, 看下就懂了, 然后呢
第二步, 在View的构造方法里解析到我们这个属性, 仔细看代码:
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray type =
context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//没有指定颜色的话默认红色
mColor =
type.getColor(R.styleable.CircleView_circle_color, Color.RED);
type.recycle();
init();
}
这段代码就是加载一个资源文件, 拿到里面的属性, 如果没有指定的话, 默认就是红色了, 那我们要使用的话, 写一个命名空间, 然后…:
<
?xml version=
"
1.0"
encoding=
"
utf-8"
?>
<
LinearLayout 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:orientation=
"
vertical"
>
<
com.liuguilin.viewwork.view.CircleView
android:layout_width=
"
match_parent"
android:layout_height=
"
100dp"
android:layout_margin=
"
20dp"
android:background=
"
#000000"
android:padding=
"
20dp"
app:circle_color=
"
@
color/colorPrimary"
/>
<
/LinearLayout>
上门的布局唯一要注意的就是这个命名空间了 xmlns:app= ”http://schemas.android.com/apk/res-auto”, 然后就可以使用app:属性的方式添加了, 那我们运行一下, 效果也很明显, 来看下全部的代码吧:
public class CircleView extends View {//颜色
private int mColor =
Color.RED;
//画笔样式
private Paint mPaint =
new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
}public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray type =
context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//没有指定颜色的话默认红色
mColor =
type.getColor(R.styleable.CircleView_circle_color, Color.RED);
type.recycle();
init();
}//初始化
private void init() {
//设置颜色
mPaint.setColor(mColor);
}@
Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode =
MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize =
MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode =
MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize =
MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode =
=
MeasureSpec.AT_MOST &
&
heightSpecMode =
=
MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode =
=
MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode =
=
MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}@
Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//padding值
int left =
getPaddingLeft();
int right =
getPaddingRight();
int top =
getPaddingTop();
int bottom =
getPaddingBottom();
//View的宽
int width =
getWidth() - left - right;
//View的高
int height =
getHeight() - top - bottom;
//圆的半径 =
宽和高比较出的数 / 2
int radiu =
Math.min(width, height) / 2;
//绘制圆
canvas.drawCircle(left +
width / 2, top +
height / 2, radiu, mPaint);
}
}
这代码清晰脱俗吧, 简单好记, 就是这样
- 2.继承ViewGroup派生出来的Layout
这个同等于自定义布局, 在之前介绍滑动的时候, 有过类似的例子, 主席就偷懒的搬上来了, 当时分析滑动冲突的两种自定义View: HorizontalScrollViewEx和StickyLayout,其中HorizontalScrollViewEx就是通过继承ViewGroup来实现的, 我们再次来分析他的测量和布局过程
这里BB一句, 要规范的写View, 需要一定的代价, 这个, 需要去看线性布局去了解了, 他们的实现都很复杂, 对于HorizontalScrollViewEx来说, 就不这么精细了
回顾下HorizontalScrollViewEx的功能, 他类似于ViewPager, 或者说水平方向的线性布局, 它内部的View可以竖直滑动, 解决他的冲突的代码就不提了, 我们主要还是看下他的测量
@
Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth =
0;
int measureHeight =
0;
final int childCount =
getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode =
MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize =
MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode =
MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize =
MeasureSpec.getMode(heightMeasureSpec);
if(childCount =
=
0){
setMeasuredDimension(0, 0);
}else if (widthSpecMode =
=
MeasureSpec.AT_MOST &
&
heightSpecMode =
=
MeasureSpec.AT_MOST) {
final View childView =
getChildAt(0);
measureWidth =
childView.getMeasuredWidth() * childCount;
measureHeight =
childView.getMeasuredHeight();
setMeasuredDimension(measureWidth, measureHeight);
} else if (widthSpecMode =
=
MeasureSpec.AT_MOST) {
final View childView =
getChildAt(0);
measureWidth =
childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSpecSize);
} else if (heightSpecMode =
=
MeasureSpec.AT_MOST) {
final View childView =
getChildAt(0);
measureHeight =
childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize, measureHeight);
}
}
这里发现一点小bug, 不过不碍事, 这里的逻辑呢, 可以这样理理, 首先有没有子元素, 没有就全部都是0, 有的话再去判断是否是warp_content,,如果是包裹内容, 那这个控件的宽度就是所以的总和了, 如果高度采用包裹内容, 那这个控件就是第一个子元素的高度, 这样说应该好理解一点
再回来说说规范性, 上面的代码可以说有两点吧, 首先, 是不应该直接设置为0, 还有就是测量的时候没有考虑到padding和子元素的maggin, 好的我们继续来看下onLayout
@
Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft =
0;
final int childCount =
getChildCount();
mChildSize =
childCount;
for (int i =
0;
i <
childCount;
i+
+
) {
final View childView =
getChildAt(i);
final int childWidth =
childView.getMeasuredWidth();
mChildWidth =
childWidth;
childView.layout(childLeft, 0, childLeft +
childWidth, childView.getMeasuredHeight());
childLeft +
=
childWidth;
}
}
这个布局的逻辑也没多少代码, 我们拿到子元素之后将其放在合适的位置, 位置是从左往右的, 但是仍然没有考虑padding和子元素的maggin, 这个也不是很规范, 好的, 那我们直接撸完整代码:
public class HorizontalScrollViewEx extends ViewGroup {private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
//分别记录上次滑动的坐标
private int mLastX =
0;
private int mLastY =
0;
//分别记录上次滑动的坐标
private int mLastXIntercept =
0;
private int mLastYIntercept =
0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}private void init() {
if (mScroller =
=
null) {
mScroller =
new Scroller(getContext());
mVelocityTracker =
VelocityTracker.obtain();
}
}@
Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted =
false;
int x =
(int) ev.getX();
int y =
(int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted =
false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted =
true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX =
x - mLastXIntercept;
int deltaY =
y - mLastYIntercept;
if (Math.abs(deltaX) >
Math.abs(deltaY)) {
intercepted =
true;
} else {
intercepted =
false;
}
break;
case MotionEvent.ACTION_UP:
intercepted =
false;
break;
}
mLastX =
x;
mLastY =
y;
mLastXIntercept =
x;
mLastYIntercept =
y;
return intercepted;
}@
Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x =
(int) event.getX();
int y =
(int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int scrollX =
getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity =
mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >
=
50) {
mChildIndex =
xVelocity >
0 ? mChildIndex - 1 : mChildIndex +
1;
} else {
mChildIndex =
(scrollX +
mChildWidth / 2) / mChildWidth;
}
mChildIndex =
Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx =
mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX =
x;
mLastY =
y;
return true;
}@
Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth =
0;
int measureHeight =
0;
final int childCount =
getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode =
MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize =
MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode =
MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize =
MeasureSpec.getMode(heightMeasureSpec);
if (childCount =
=
0) {
setMeasuredDimension(0, 0);
} else if (widthSpecMode =
=
MeasureSpec.AT_MOST &
&
heightSpecMode =
=
MeasureSpec.AT_MOST) {
final View childView =
getChildAt(0);
measureWidth =
childView.getMeasuredWidth() * childCount;
measureHeight =
childView.getMeasuredHeight();
setMeasuredDimension(measureWidth, measureHeight);
} else if (widthSpecMode =
=
MeasureSpec.AT_MOST) {
final View childView =
getChildAt(0);
measureWidth =
childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSpecSize);
} else if (heightSpecMode =
=
MeasureSpec.AT_MOST) {
final View childView =
getChildAt(0);
measureHeight =
childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize, measureHeight);
}
}@
Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft =
0;
final int childCount =
getChildCount();
mChildrenSize =
childCount;
for (int i =
0;
i <
childCount;
i+
+
) {
final View childView =
getChildAt(i);
final int childWidth =
childView.getMeasuredWidth();
mChildWidth =
childWidth;
childView.layout(childLeft, 0, childLeft +
childWidth, childView.getMeasuredHeight());
childLeft +
=
childWidth;
}
}private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}@
Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}@
Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
OK, 代码慢慢看四.自定义View的思想
【Android艺术开发探索第四章——View的工作原理(下)】整体来讲, 还是有点模糊, 不过精髓都已经体现出来了, 自定义算是一个综合体系, 大多数情况下还是要灵活一点, 而且有些可能需要公式计算, 所以比较五花八门, 那我们这里肯定不能一一去概括了, 但是基本功大家应该都已经了解了, 我在后续的章节中会挑一些好的View来介绍, 这是我, 不是书上的, 最主要的是基本功然后就是实现思路了, 这点我特别推荐去学习优秀的开源库了解一下, 好了, 我们第四章, View的工作原理到这里就GG了, 下章再见! ! !PPT:http://download.csdn.net/detail/qq_26787115/9699388 MakeDown:http://pan.baidu.com/s/1o7Z4Djs 密码: xdgt Sample: http://download.csdn.net/detail/qq_26787115/9699383 我正在参加2016博客之星, 请投我一票吧!
推荐阅读
- Android开发---MediaPlayer简单音乐播放器
- Android 数据库读取数据显示 [5]
- Android关于Theme.AppCompat相关问题的深入分析(转)
- Android ImageView 正确使用姿势
- Android笔记——Application的作用
- Android studio关于真机调试DDMS中的data文件夹打不开的解决方法
- 灵活运用模板在Word2010中建文档的秘技_Word专区
- 在Word2010中保存文档的技巧汇总_Word专区
- Word 2010中调整自动保存时间间隔的攻略_Word专区