一个设计的好的自定义view,有着丰富的特性和简单的接口。它可以有效的使用cpu和内存。
所以一个自定义view必须满足以下几点:
android 框架提供了一些基本的类和XMl标签来帮助你创建一个合适的自定义view, 那么首先
所有的view类第一都是继承与view类的,你的自定义View可以直接继承 view,你也可以继承一个 view 的子类,比如:Button
为了让Android Studio可以和你的 View 交互,你的类中 至少要实现一个 * 把context 和 AttributeSet 作为参数的构造函数 *,这个构造函数可以使得 layout editor 可以编辑一个你的View的实例
class PieChart extends View {
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
需要在 xml 文件里 明确的用 attr 元素 指出 自定义控件 的表现和行为,一个良好设计的 view 是可以通过 xml 文件 添加 和 改变的,你需要如下几步:
添加在 res/values/attrs.xml
文件中
中,为你的view自定义属性如下:
<resources>
<declare-styleable name="PieChart">
<attr name="showText" format="boolean" />
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
attr>
declare-styleable>
resources>
这个代码有两个 属性, showText
和 labelPosition
属于一个 名字为 Piechat 的 styleable entity
依照惯例来说,这个自定义类的 名字应该和 styleable entity的名字相同
一旦你定义了自定义属性,你就可以在 layout xml 文件中使用,如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
<com.example.customviews.charting.PieChart
custom:showText="true"
custom:labelPosition="left" />
LinearLayout>
当一个view在layout文件中添加之后,这个xml文件中所有的 属性,都可以被resource bundle 读取并以 AttributeSet 的形式,传入 view 的构造函数中
如下代码:
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.PieChart,
0, 0);
try {
mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
} finally {
a.recycle();
}
}
可以看到,我们并没有直接的从 AttributeSet 对象中获取属性,使用obtainStyledAttribute()
方法,获取到一个 TypeArray 中。这样的好处在于,可以避免一些缺陷的发生,比如资源引用是不可用的
属性虽然好,但是在 view 初始化的时候,属性都是只读的。为了提供动态行为,每个属性必须对应一个 getter 和 一个setter 函数, 如下:
public boolean isShowText() {
return mShowText;
}
public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}
在设置中调用的两个函数各有什么用呢?
重要!
一旦你的属性改变了,说明你的view外观要改变,这时要调用 invalidate()
使view无效化,然后你需要 requestLayout
请求 新的 Layout ,重绘制界面
自定义view 应该 同样的 支持事件监听器 ,比如,PieChart 就支持一个 自定义事件 OnCurrentItemChanged
来修改监听者,使得用户可以循环到下一个 pie sllice上
自定义控件最重要的部分大概就是 外观了,根据你的app需求,自定义 绘制 可以很简单也可以很复杂。继续了解吧!
绘制一个 自定义view 最重要的部分就是重写 onDraw() 方法了。onDraw()
方法的参数是一个 Canvas 对象,可以用来绘制它自己。
在你调用任何Canvas中的绘制方法之前,创建一个 Paint 对象非常重要,因为
Canvas 决定画什么,而 Paint 决定怎么画(比如Canvas决定你想在屏幕上画的形状,而Paint定义颜色,字体,风格,阴影等)
代码如下:
private void init() {
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
if (mTextHeight == 0) {
mTextHeight = mTextPaint.getTextSize();
} else {
mTextPaint.setTextSize(mTextHeight);
}
mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPiePaint.setStyle(Paint.Style.FILL);
mPiePaint.setTextSize(mTextHeight);
mShadowPaint = new Paint(0);
mShadowPaint.setColor(0xff101010);
mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
...
提前创建好这些 Paint 对象是非常重要的优化,因为Views重绘非常的平凡,但是很多drawing 对象初始化过程是 花销很多的, 在你的 onDraw()
方法中来创建drawing对象的话,那么会降低性能,UI变卡。 最好提前全部创建好
为了绘制合适,在绘制以前你得知道 大小尺寸 是多少。复杂的view需要执行 多次 布局 计算,永远不要猜你的view有多大,就算只有一个app在使用你的view,这个app也必须处理不同的屏幕尺寸,方向。
虽然View有很多处理测量的方法,但是大部分都需要重写。如果你的view不需要特别的控制大小,那么你需要重写一个一个 方法 onSizeChanged()
不是在绘制时计算,而是在改变时计算
onSizeChanged()
方法在你的View第一次被 指定一个尺寸的时候被调用,如果你的view的尺寸改变了,那么计算位置,面积等其他的值,都在这个 onSizeChanged()
方法里执行,而不是每次 绘制都重新计算一遍。(在 piechart案例中,就是在onSizeChanged方法里计算矩形边际的改变等)
默认是计算padding
当你指定一个 size 的时候,layout manager 是假定你view的所有padding是包含在内的,在你计算view的尺寸的时候,你必须处理这些 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;
// Figure out how big we can make the pie.
float diameter = Math.min(ww, hh);
如果你的view的自定义程度要求很高!,那么就实现 onMeasure()
方法。这个方法的参数 View.MeasureSpec
就是父view 想让 你的view显示的大小。
这些值是存在 包装过的 integer 对象中,作为优化,你可以使用View.MeasureSpec
静态方法将 这些 信息 解出来,如下:
@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);
}
这段代码目的在于将 Pie的大小设为和 lable 的一样大,值得注意的几个部分:
resolveSizeAndState()
方法用来创造一个 final 的 宽 和 高 的 值将,将 view 期望的值 和 传入 onMeasure 方法的值比较,返回一个合适的onMeasure()
方法是没有返回值的,但是强制性的 调用 方法setMeasureDimension()
来传递结果一旦绘图对象创建好了,measure也定义好了。那么就可以实现 onDraw()
方法了。每一个view实现 onDraw()
方法都是不同的,但是有一些相同的操作
DrawtText()
,设置字体使用 setTypeface()
,设置字体颜色使用 setColor()
drawRect()
drawArc()
等,填充和外边都是调用 setStyle
设置drawPath()
绘制setShader()
方法来使用你的 LinearGradient 填充形状drawBitmap()
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the shadow
canvas.drawOval(
mShadowBounds,
mShadowPaint
);
// Draw the label text
canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);
// Draw the pie slices
for (int i = 0; i < mData.size(); ++i) {
Item it = mData.get(i);
mPiePaint.setShader(it.mShader);
canvas.drawArc(mBounds,
360 - it.mEndAngle,
it.mEndAngle - it.mStartAngle,
true, mPiePaint);
}
// Draw the pointer
canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}
绘制出UI只是创建一个自定义view的一部分,还有一部分就是让你的View响应用户的输入。
就像其他的UI框架一样,android的支持输入事件模型。用户的行为会转为开大回调的事件 。 然后你重写回调就可以 自定义 你的应用应该如何对用户的 行为作出反应了。
android系统中最常见的输入事件就是 touch 事件了,重写 onTouchEvent(MotionEvent)
来 处理事件
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
但是touch事件是 一般是没有什么用的,手势交互发展到现在有多种多样,比如 tapping 轻敲,pulling 下拉,pushing 推……等等
为了将 未加工 的touch 转为 加工过的手势,android 提供了 GestureDetector
为了构造一个 GestureDetector ,你需要两个参数
一个继承了 GestureDetector.SimpleOnGestureListener
(或者 GestureDetector.OnGestureListener)的实例,作为手势的监听器。还有我们的自定义类
class mListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());
不管你是否实现 GestureDetector.SimpleOnGestureListener
,实现的 onDown()
方法一定要 return true
。这个步骤是必要的,如果你返回一个 false,那么系统会认为你想忽略掉 剩下的手势 手势的判定,那么其他的手势就再也无法取得回调了。
只有当你真的确定 只有某个手势 的时候,你才可以如上使用
一旦你创建好了一个 GestureDetector
那么你就可以在 onTouchEvent 中解析这个touch事件了
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}
当你传入的这个 touch event 并不能解析为 手势的任何一种,那么你就可以执行你自定义的手势探测代码。
android 还提供了一些 类 比如 Scroller 可以让实现一些物理的效果 比如滑动
onDraw()
方法和 动画running的时候分配内存,这样垃圾回收会造成卡顿onDraw()
方法,大部分对 onDraw()
调用都是 invalidate()
,所以尽量不使用 invalidate() 方法requestLayout()
的每次调用,Android UI 系统都要遍历整个 view 层来找到 整个view所需的 空间大小。如果产生冲突,可能还得多次遍历。所以尽可能的让你的UI分布扁平