自定义View#
基本约束##
Conform to Android standards
Provide custom styleable attributes that work with Android XML layouts
Send accessibility events
Be compatible with multiple Android platforms.
1. 符合Android标准
2. 提供一些自定义的样式属性,可以在layout中配置
3. 实现自己的events
4. 兼容Android各平台
1.1 继承一个 View
所有framework中提供的View类都继承自View。我们的自定义View可以直接继承View,也可以继承View的子类,比如Button,TextView
必须实现下面的构造函数
class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
1.2声明自定义属性##
1.2.1 在资源元素中定义我们的自定义属性###
res/values/attrs.xml
这部分代码定义两个属性: showText和labelPosition,属于MyView.这里的name最好跟我们自定义的View名字相同,不过这不是强制的,但是正常开发中一般都这么做。
完成上面的xml后,我们就可以在layout中给这些属性赋值。就像Android提供的原生属性一样,唯一不同的是我们的自定义属性属于另外一个namespace.
http://schemas.android.com/apk/res/com.gome.farmpatner
默认的命名空间是
http://schemas.android.com/apk/res/android
根据上面的例子,在layout文件中,可以这样定义属性值
注意到这里,对于自定义控件我们引用的是全包名。
如果MyView是另一个类CustomizedView的内部类,那么需要这么写:
com.gome.farmpatner.CustomizedView$MyView
1.2.2 应用自定义属性###
当View从XML中创建之后,所有的属性值都会从resource bundle中读出来并存储到一个AttributeSet中,这个AttributeSet最终会传给我们view的构造函数。
应该使用Android提供的接口去解析AttributeSet,而不是直接读取它,因为直接读取有两个缺点:
a. 属性值的类型需要自己解决
需要手动解决资源值的类型getAttributeResourceValue(int, int),还有资源的查找也需要自己解决
具体的可以看http://192.168.63.218:8080/source/xref/GM025_CT_S06/frameworks/base/core/java/android/util/AttributeSet.java#20
https://developer.android.com/reference/android/util/AttributeSet.html
b. 样式需要自己去应用
Android提供的接口会帮我们apply这些属性到样式中。
正确的使用方式是:
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.MyView,
0, 0);
try {
mShowText = a.getBoolean(R.styleable.showText, false);
mTextPos = a.getInteger(R.styleable.labelPosition, 0);
} finally {
a.recycle();
}
}
记得TypeArray最后需要recycle()。
1.2.3 添加属性和事件###
Attributes可以很方便的控制view的显示和行为,不过这些值只能在view初始化完成之后才能获取的到。
一般会提供一个动态的接口让调用者去控制。也就是getter和setter。
public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}
注意setter方法中,最后调用了invalidata和requestLayout,这样View才会重新绘制和布局,才能将调用者想要的效果立马显示到View中。
如果属性影响到View的展示,那么我们一定得调用invalidate()来通知系统对View进行重绘。
如果属性值影响到view的大小或者形状等布局类的内容,则一定要调用requestLayout来通知系统对View进行重新布局。
在自定义View中,可以根据需要暴露出一些event的相关接口,提供一个listener的interface供调用者实现。
对于本章节,最基本的规则就是:我们应该将那些会影响到View的展示和行为的property都给暴露出来。
1.2.4 Design For Accessibility###
这主要是Google提出来,为了残障人士准备的。一些残疾人可能看不见或者使用不了触摸屏的用户。
这部分内容具体可以看看https://developer.android.com/guide/topics/ui/accessibility/apps.html#custom-views
1.3 实现自定义的绘制
1.3.1 Override onDraw
onDraw(Canvas canvas)
在使用canvas绘制之前,我们先得有paint对象,下面就是paint的相关介绍
1.3.2 创建需要绘制的对象
android.graphics包将绘制的工作分为两部分:
a. 画什么, canvas
b. 怎么画, paint
canvas决定需要绘制的形状,而paint则定义颜色,样式,字体等内容。
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
if (mTextHeight == 0) {
mTextHeight = mTextPaint.getTextSize();
} else {
mTextPaint.setTextSize(mTextHeight);
}
提前创建paint对象是至关重要的优化手段,因为View会被重绘的很频繁,如果我们每次都在onDraw中创建对象的话会相当影响程序性能。
1.3.3处理Layout相关事件###
为了能够准确的绘制我们的自定义View,就得知道size是多大。
复杂一点的自定义View经常需要根据size和处于屏幕中的位置来执行多次layout计算。我们绝不可以假设我们的view在屏幕中占多大位置。即便是只有一个app使用我们的view,但是该app也得处理不同的屏幕尺寸,不同分辨率,以及横竖屏这些不同情况下的展示。
如果没有特别的需求,只要override onSizeChanged函数就好了。
当view的size确定后,因为一些原因size发生了变化,这时候会调用onSizeChanged().在onSizeChanged()里面计算位置、尺寸以及其他任何和view的size相关的值,尽量不要在draw绘制的时候去重新计算。
一旦view的size被赋值之后,layout manager就会假定这个size是包括了padding内边距的值。所以我们在计算view的size的时候必须处理padding的值。可以看下面的例子
// Account for padding
float xpad = (float)(getPaddingLeft() + getPaddingRight());
float ypad = (float)(getPaddingTop() + getPaddingBottom());
// Account for the label
if (mShowText) xpad += mTextWidth;
float ww = (float)w - xpad;
float hh = (float)h - ypad;
// 计算出直径
float diameter = Math.min(ww, hh);
==》如果想要更好的控制layout的参数,可以复写onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
MeasureSpec将mode和value打包进一个int中了,可以用位移操作获取对应的值,不过MeasureSpec已经提供了对应的方法。
getMode(int measureSpec)
Extracts the mode from the supplied measure specification.
getSize(int measureSpec)
Extracts the size from the supplied measure specification.
有三种模式: 英文比较好理解
AT_MOST child can be as large as it wants up to the specified size
EXACTLY The parent has determined an exact size for the child.
UNSPECIFIED The parent has not imposed any constraint on the child.
具体看一下一个复写该方法的例子:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Try for a width based on our minimum
int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
int w = resolveSizeAndState(minw, widthMeasureSpec, 1);
// Whatever the width ends up being, ask for a height that would let the pie
// get as big as it can
int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);
setMeasuredDimension(w, h);
}
-> minw的计算包含了padding,就跟上面onSizeChanged()提到的一样
-> resolveSizeAndState用来确定最终的宽高的大小
--------之前看过几本书中,都是自己根据MeasureSpec的mode去计算,其原理跟Android提供的函数resolveSizeAndState是一样的。
-> onMeasure没有返回值,该方法使用setMeasuredDimension来传递结果,调用该方法是必须的,否则,会抛出异常。
1.3.4 Draw 绘制
一旦你初始化了一些必须的object,比如paint什么的,你就可以实现自己的onDraw函数了。
虽然每个view的绘制过程都不一样,不过都有几个通用的接口:
drawText: 绘制textsetTypeface设置字体,setColor设置颜色
drawRect、drawOval、drawArc: 绘制简单的形状,使用setStyle来设置是否填充内部,外边线的绘制
drawPath: 绘制更为复杂的形状,通过添加直线和曲线来创建一个自定义的Path对象,然后使用drawPath()绘制到view上,Path也可以使用setStyle.
setShader()&LinearGradient: 使用LinearGradient对象设置渐变填充,然后调用setShader来将LinearGradient应用到对应的shape中。
drawBitmap: 绘制bitmap
1.4 处理用户交互
1.4.1 Input Gestures
用户的操作会触发相关的回调函数,并传入相关的events,我们可以复写这些callbacks来实现我们跟用户交互的逻辑。
1.4.1.1 touch events
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
MotionEvent有TOUCH_DOWN, TOUCH_MOVE, TOUCH_UP
1.4.1.2 gesture
touch event比较简单,还有一些手势的event,包括tapping(按压,长按?), pulling, pushing, flinging(抛,类似listview的滑动?), and zooming(缩放). Android提供了GestureDetector。
我们需要实现GestureDetector.OnGestureListener接口,来实现自己的处理逻辑。如果我们仅仅是想处理部分的手势逻辑,那么我们可以选择继承GestureDetector.SimpleOnGestureListener. 下面就是一个例子:
class mListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());
onDown的return true代表我们的gesture希望处理接下来的一系列的事件,因为不管是touch还是gesture肯定都是以一个Down的操作开始的。如果这里return false,那么mListener其他的处理函数都不会被调用。
下面的代码在onTouchEvent中判断gesturelistener是否需要处理该事件
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}
1.4.2 Create Physically Plausible Motion (创建模拟物理的动作)
gesture是一种比较强大的控制屏幕操作的方案,不过比较难以记忆,除非提供出一种物理上合理的操作。比如listview用力滑动一下,然后抬手,listview还会继续滑动一定的距离,就类似物理上的惯性。Android中的一个例子就是fling gesture.
Scroll类是处理fling gesture的基础
想要开始一个fling(就是一个抛动,在屏幕上快速滑动然后抬起手指),可以调用fling,参数是starting velocity(开始的速度),最大最小的xy坐标值。velocity的值我们可以直接提供GestureDetector计算给我们的。
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
postInvalidate();
}
Tips:尽管GestureDetector提供的velocity在物理上是精准的,不过实际上会发现这个值会让滑动变得很快,所以一般我们都会将velocityX和velocityY除以一个4或者8.
-->fling()函数帮助我们建立了fling的物理模型,然后,我们需要每隔一段时间调用Scroller.computeScrollOffset()来更新scroller。computeScrollOffset会通过物理模型计算出xy坐标的位置,然后更新scroller的内部状态。可以调用getCurrX()和getCurrY()获取到对应的值。
大部分view都是直接将Scroller的xy值传递给scrollTo。当然也可以使用其他动画,比如rotate。
if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
Scroll会为我们计算出scroll的位置,但是它并不会自动把这些改动apply到View上。我们应该做的是确保以足够的频率来get和apply新的坐标到view上,这样滚动的动画才会平滑。一般,有两种方式来实现:
-->在fling后调用postInvalidate(),强制重新绘制view,这种情况下我们就需要在onDraw中计算scroll的offset,并且每当offset改变的时候都要调用postInvalidate().
-->设置一个ValueAnimator,处理fling的过程,添加一个listener处理fling动画的update,addUpdateListener.这种方式避免有时候view不必要的重绘。3.0之后支持
mScroller = new Scroller(getContext(), null, true);
mScrollAnimator = ValueAnimator.ofFloat(0,1);
mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
} else {
mScrollAnimator.cancel();
onScrollFinished();
}
}
});
1.4.3 平滑动画
Android属性动画 property animation framework.
每当我们有什么属性发生变化的时候,并不是直接更新到view上去,而是使用valueAnimator去操作。
mAutoCenterAnimator = ObjectAnimator.ofInt(MyView.this, "testValue", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();
Tips:这部分后面研究
如果我们改变的是view的基本属性值,那么就很简单,直接使用Android封装好的接口:
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();
1.5 View的优化##
要想界面流畅不丢帧,那么就要保证1秒钟60帧左右。
为了增加View的流畅度,那么就要把一些不必要的代码从需要频繁调用的代码中剥离出来。
-->先从onDraw开始,下面的策略会带来很大的回报。首先应该避免在onDraw中创建对象,因为局部变量的allocation会频繁唤醒GC从而有可能造成界面的卡顿。可以在初始化的时候或者多个动画之间的时候去分配内存,但是切记不要在动画执行的时候去执行allocation。
-->另外,尽量减少onDraw不必要的调用,大部分onDraw的回调都是因为invalidate()的调用,所以要减少invalidate()不必要的调用。
-->另外一个耗时操作是布局的遍历。当我们调用requestLayout的时候,Android需要遍历整个view的层级去确定每个view的size。如果存在一些冲突,那么可能会多次遍历。保证你的ViewGroup的层级尽可能的少。
-->如果你要实现的是一个很复杂的UI,应该考虑自定一个ViewGroup。不同于自带的views,自定义view可以根据应用场景对子view的大小和位置做一些预设和假定,因此会一定程度上避免多次遍历children来layout。
比如把子view的大小和位置直接写死,就不需要measure子view了。
参考:https://developer.android.com/training/custom-views/index.html