您的位置:首页 > 编程语言 > Go语言

Google教我如何定制自己的View

2016-09-12 19:15 411 查看
前言

今天我看了Google教程中有关定制View的相关内容,这是之前从来没有接触过的领域,在github上能经常看到一些大神自定义的View,比如按钮,ListView,好像他们天生就可以随心所欲的定制自己的View,而自己也不知道如何入门,今天再Google上碰到了这一节,就心血来潮的看了看,Google只讲了大概,看完还不是很懂,不过也学到一些东西。这一块的内容还是在理解的基础上多看看代码才能明白呀。

正文

在项目里,我们经常会遇到一些需求是Android内置View无法满足我们的,这时我们就需要定义自己的View。

创建一个View类

一个设计良好的类包含很多容易使用的功能,而且也能有效的利用CPU和内容资源,它还应该具有以下的特性:

符合Android标准

提供XML可指定的自定义属性

发送事件

兼容多种Android平台

Android提供了基本一些基本类和XML标签能够帮助我们指定符合这些要求的类。

定义一个View子类

Android里面所有的View类全部都继承于View类,所以你也应该去继承View类,当然也可以为了节约时间去继承已有的View类,比如说Button。

为了让Android Studio去和你的View交互,你至少应该提供一个构造函数,把Context和AttributeSet对象作为参数,这个构造函数能够允许让布局编辑器去创建和编辑你的View实例。

class PieChart extends View {
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
}


定义自定义的属性

为了把一个内置的View添加到UI,你应该通过XML元素的方式制定它并且通过元素属性控制它的外观和行为。定义良好的View可以通过XML的方式添加和制定样式。为了在View中做到这一点,你应该:

通过一个< declare-styleable >资源元素去指定自定义View的属性

确定XML布局文件里的属性值

在运行时重新获取这些属性值

把这些属性值应用给你的View

这节讨论如何去定制这些属性和确定他们的值,下一节会讲如何重新获取并在运行时应用他们。

为了定义一个自定义的属性,添加< declare-styleable >元素到你的项目里。习惯上一般都把它放到res/values/attrs.xml文件里,这里有一份XML文件的样例:

<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,属于一个名叫PieChart的自定义的实体。按照惯例,这个名字应该和自定义View的类名保持一致。但也不是必须要严格的遵循这个管理,许多流行的代码编辑器都依靠它自己的命名惯例去命名。

一旦你定义好了一个定制属性,你就可以在xml文件里面像内置属性一样使用它们了。唯一的不同就是这些内置属性属于另一个不同的命名空间。不是我之前经常见的http://schemas.android.com/apk/res/android命名空间了,它们属于http://schemas.android.com/apk/res/[你的包名]。举一个例子,如何应用PieChart的属性:

<?xml version="1.0" encoding="utf-8"?>
<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>


为了避免去重复很长的命名空间URI,这个例子使用了xmlns指令。这个指令会给http://schemas.android.com/apk/res/com.example.customviews分配一个别名。你可以任意的去为你的命名空间选择你想要的别名。

注意一下把自定义View添加到布局文件里的XML标签。它应该是自定义View类的完全限定名。如果你的View类是一个内部类,你就要用外部类来进一步的限定它。举一个例子,如果PieChart由一个内部类叫做PieView。为了去使用这个类里面的自定义属性,你应该使用这个标签——com.example.customviews.charting.PieChart$PieView。

应用自定义属性

当一个view从XL布局文件里面创建,它标签下面所有的属性都可以从resource bundle里面读取并传递给view的构造函数作为一个AttributeSet。虽然我们可以直接从AttributeSet里面直接读取,但是这么做有一些缺点:

带有属性值的资源引用不能解决

样式不能得到应用

如果我们把AttributeSet传递给obtainStyledAttributes()方法。这个方法会回传一个TypedAttay类型的数组,它们的值已经被重新引用过并样式修改过

Android的编译器为我们使用obtainStyledAttributes()做了很多工作,对于每一个< declare-styleable >资源文件,产生的R文件定义了一个数组作为属性的ID,也定义了一个数组为每一个属性定义它们的标号,通过常量集合的形式。你可以通过提前定制好的常量去从TypedArray里面读取属性。下面是PieChart如何读取它的属性:

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();
}
}


注意:TypedArray是一个可共享的资源,我们用完之后应该及时回收。

添加属性和事件

属性是一个控制view外观和行为的一种有效的方式,但是他们只应该在view初始化的时候读取。为了提供动态的行为,我们应该给每一个自定义属性提供getter和setter方法对。下面的片段展示了PieChart如何暴露一个叫showText的属性:

public boolean isShowText() {
return mShowText;
}

public void setShowText(boolean showText) {
mShowText = showText;
invalidate();
requestLayout();
}


注意到setShowText调用了incalidate()和requestLayout()。这些调用对于确保view行为的可靠是很重要的。在可能会影响view的属性发生改变之后,你应该让这个view无效,这样系统才知道这个view需要重绘。比如,如果一个属性的改变可能会应该这个view的大小和形状,你应该去请求一个新的布局。忘记这些方法的调用会造成很难发现的bug。

自定义View该应该支持事件监听器以便和重要事件进行交流。比如说,PieChart暴露了一个自定义的事件叫做OnCurrentItemChanged去提醒监听器,用户旋转了饼图去注意饼图的另一个部分。

当我们是这个自定义View的唯一使用者时,我们很容易就忘记去暴露这些属性和事件。花一些实践去仔细的确定view的接口可以降低未来的维护成本。一个值得我们去遵循的好的规则是,总是去曝光那些会影响View外观和行为的属性。

为可使用设计

Your custom view should support the widest range of users. This includes users with disabilities that prevent them from seeing or using a touchscreen. To support users with disabilities, you should:

你自定义的View应该要支持大范围的用户。包括缺乏看或是使用触屏设备的用户,为了支持这些用户,我们应该:

Label your input fields using the android:contentDescription attribute

Send accessibility events by calling sendAccessibilityEvent() when appropriate.

Support alternate controllers, such as D-pad and trackball

这三段不太懂,大家自行翻译吧。

自已画图

自定义View最重要的部分就是它的外观了。自定义画图可以简单也可以很复杂,根据你的项目需求不同而不同。

覆写 onDraw()方法

覆写onDraw()方法是画自定义View的一个最重要的步骤。传递给onDraw(0的参数是一个Canvas对象,view可以用它去画它自己,这个Canvas类定义很多方法,能画线,画字,画图以及很多主要的图形。你在onDraw()方法里用这些方法创建你的自定义UI。

在你调用任何方法之间,创建一个画笔对象是很重要的。下面会讨论。

创建用于绘画的对象

android.graphics框架把绘图分为两类:

画什么,由Canvas解决

怎么画,用Paint解决

举一个例子,Canvas提供一个画线的方法,而Paint提供了定义线的颜色的方法。Canvas提供了画长方形的方法,而Paint可以决定是否要填充这个长方形或者就让它空着。简而言之,Canvas定义你想画在屏幕上的形状,而Paint定义了颜色,样式,字体和很多你想要画的形状的方方面面。

所以在你画任何东西之前,你应该创建一个或多个Paint对象。这个PieChart的例子中通过Init()函数完成这一操作,并在构造函数里调用init()方法。

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));

...


提前创建好对象是一个很重要的优化。View会被经常性的重绘,所以很多用于画图的对象需要昂贵的开销。把创建绘图对象放在onDraw()方法里会明显降低性能,并且会使你的UI变的拖沓。

处理布局事件

为了恰当的去重绘你的自定义View,你应该需要知道它的尺寸有多大。复杂的自定义View经常需要依据它们在屏幕上的大小和形状进行多次布局计算。你应该永远不要做出你屏幕上的view尺寸大小的猜想。即使只有一个APP使用你的自定义View,这个APP也需要处理不同的屏幕大,多样的屏幕密度和在横向与纵向模式下不同的比例。

虽然View有很多方法都可以用来处理测量,但是它们中的大多数方法都不需要被覆写。如果你的View不需要特别控制它的大小,你只需要覆写一个方法——onSizeChanged()。

onSizeChanged()方法在你的View第一次被分配一个大小时会被调用,如果因为任何原因,你的View大小改变,这个方法还会再次被调用。在onSizeChanged()方法里计算位置,计算好其他任何你的View大小相关的值,而不是在重绘的时候再取计算。在PieChart的例子里,当计算边界角度和文本或是其他可视性元素的位置时onSizeChanged()方法会被调用。

当你为你的View分配一个尺寸时,布局管理器会假设这个尺寸包含了View的所有padding。你必须处理这些padding值当你计算View尺寸的时候。这里由一个PieChart.onSizeChanged()的代码片段向你展示怎么去做:

// 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是多大和是否这个大小是一个硬编码的最大值还只是一个建议值。作为优化,这些值应该被储存在打包过的整数里,你应该使用View.MeasureSpec里面的静态方法去解压这些储存在每个数字里的信息。

这里有一个onMeasure()执行的例子。在这个执行里,PieChart会让这个区域尽可能大使得这个pie和它的标签一样大:

@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);
}


在这份代码里有三个地方需要注意:

计算尺寸应该考虑view的padding,如同之前提到过的,这是这个view的责任。

resolveSizeAndState()这个帮助方法是用来创建常量的宽度值和高度值。通过比较view希望得到的大小和传递给onMeasure()方法的spec,这个帮助方法会返回一个合适的View.MeasureSpec值。

onMeasure()方法没有返回的值,它的结果是通过setMeasureDimension()来传递。必须强制的调用这个方法。如果你忽略了这个方法的调用,View类就会抛出runtime异常。

画图!

一旦你完成了绘画对象的创建并测量好了View的代码,你就可以在onDraw()方法里面执行它。每个View的onDraw()方法都各不相同,但是它们之间有一些常用的操作:

用drawText()画字,用setTypeface()确定字形,用setColor()确定字的颜色

用drawRect(),drawOval(),drawAcr()来画一些原始的形状。用setStyle()去决定这些形状是否被填充,描绘外边框。

用Path类来画更加复杂的形状。通过给path类添加直线和曲线来定义一个形状,然后用drawPath()来画出来。如果只是一些简单的图形,可以用setStyle()确定path是否描绘外边框是否被填充。

创建LinearGradient对象来定义渐变填充。用你的LinearGradient调用setShader()方法去填充形状。

使用drawBitmap()去画bitmap

下面是画piechart的代码,展示如何画字,线和形状:

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);
}


让View变得交互

画出UI只是创建自定义View的一部分,你也需要像在真实世界里那样让你的View相应用户的输入。对象影响表现的像真实世界里面的物体一样。比如说,图片不能突然出现或是消失,因为真实世界里不会那样,图片应该从一个地方移动到另一个地方。

用户也会察觉到这种微妙的行为,最好表现的和真实世界里面一样。举一个例子,当用户移动一个UI对象,他们应该能在物体开始运动的时候感觉到摩擦力,在手指脱离后,物体还有继续向前滑动的冲力。

处理输入手势

和很多UI框架一样,Android也支持一个输入事件的模型。用户的行为会转化为一些事件,这些事件能够触发回调方法,你可以覆写这些回调方法去定制你的应用如何相应用户输入。Android系统里最常用的输入就是接触,它会触发onTouchEvent(android.view.MotionEvent)方法。覆写这个方法就能处理这个事件:

@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}


接触事件本身并不是很实用。现代的接触UI定义大多考虑了这些手势,轻碰,拉,推,快速滑动和缩放。为了把这些原始的接触事件转化为手势,Android也提供了GestureDetector。

构造一个GestureDetector需要传递一个继承了GestureDetector.OnGestureListener的类实例。如果你只是想要处理一些简单的手势,你可以基层GestureDetector.SimpleOnGestureListener,而不是继承GestureDetector.OnGestureListener接口。举一个例子,这个代码创建了一个类继承GestureDetector.SimpleOnGestureListener并且覆写了onDown(MotionEvent)。

class mListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());


不管你是不是要用GestureDetector.SimpleOnGestureListener,你总是要实现onDown()方法并返回true。这一步是必须的,定位所有的手势都是从onDown()方法开始。如果你在onDown()方法里面返回false,系统会认为你想要忽略接下来所有其他的手势,在GestureDetector.OnGestureListener里面的方法永远不会被调用。只有你真的想忽略接下来所有的手势时,你才能返回false。一旦你实现了GestureDetector.OnGestureListener接口,并且创建了一个GestureDetector实例,你就可以用GestureDetector去翻译你在onTouchEvent()方法里接收到的事件了。

@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}


如果你传递给onTouchEvent()方法的接触事件不认为这是手势的一部分的话,它就会返回false,你接下来就可以运行自己自定义的手势检测代码了。

创建看起来正确的运动

手势在控制触屏设备是一个很重要的方式,但是他们也可以是意料之外的,很难去记住,除非它们会产生看起来正确的结果。一个很好的例子就是快速滑动手势,当用户从屏幕的一侧滑动滑动手指然后抬起她,这个手势应该要达到这样的效果——检测到快速滑动以后UI会马上相应并快速滑动,然后慢下来,就像用户在推动一个飞轮,并使它旋转。

然而模拟这样的效果并不是琐碎的,许多物理和数学都需要去完成这个任务。幸运的是,Android提供了帮助类去模拟这个效果和一些其他的行为。Scroller类就是处理飞轮滑动效果的基础。

为了开始一个滑动,我们应该调用fling()方法,传入一个速度,最大的x,y值和最小的x,y值。对于速度的值,你也可以用在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();
}


注意:GesturDetector计算的速度值是很准确的,但是许多开发者会认为这个值会让滑动太快,所以一般都除以4~8的一个因子。

fling()方法的调用建立起可滑动手势的物理模型。然后你需要在一个固定区间通过调用Scroller.computeScrollOffset()方法更新Scroller。computeScrollOffset()通过读取当前时间,并用物理模型去计算那个时间的x,y的位置,去更新Scroller对象的状态。调用getCurrX()和getCurrY()去获取这些值。

大多数View直接把Scroller对象的x,y位置直接传递给scrollTo()。这个piechart有些不同:它用当前的y去设置需要旋转的角度。

if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
setPieRotation(mScroller.getCurrY());
}


Scroller类能帮你计算好位置,但是它不会自动帮你把位置应用到你的view上。你需要确保你获取并应用新坐标的频率足够大来确保你的滚动看起来足够平滑。这里有两种方法:

调用postInvalidate()方法在fling()之后,去强制重绘这个技术需要你在OnDraw()方法里面计算好偏移量,每次当偏移量改变时调用postInvalidate()。

建立一个ValueAnimator给fling计算过渡动画,调用addUpdateListener()去添加一个监听器处理动画更新。

PieChart使用的第二种方法,这个方法更复杂,但是和动画系统能更好工作,不会造成潜在不必要的无效view。这个方法在API11之前是没有的,所以我们应该在运行时确定一下系统的版本。

       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(); setPieRotation(mScroller.getCurrY()); } else {
mScrollAnimator.cancel();
onScrollFinished();
}
}
});


让你的过渡平滑

用户期望的是在各状态之间的过渡是平滑的。UI元素会隐隐出现,慢慢消失,而不是突然出现,突然消失。运动的开始和结束应该是平滑的,而不是突然开始,突然停止。Android的动画框架在Android3.0被引入,使得产生平滑的过渡。

为了去使用动画系统,何时一个属性的改变会影响你的view的外观,都不要直接去改变那个属性,而是应该用ValueAnimator去做这个改变。在下面的例子里,修改PieChart中已经选中的Pie片会导致整个饼图开始旋转。ValueAnimation会在数百毫秒内产生这样一个过渡效果,而不是马上设置一个新的旋转值。

mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();


如果你想改变的基本View的属性之一的话,做这个动画会更加方便。因为View有内置的ViewPropertyAnimator属性。

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();


优化View

现在你有一个设计良好的View能相应用户手势和在状态之间过渡,确保这个View快速运行。为了避免UI觉得拖拉,你应该确保动画应该保持在每秒60帧。

做的少一些,调用少一些

为了加速你的View,消除一些不应要的但是会被经常调用的代码。从onDraw()开始,这里是回调次数最多的地方。特别的,你应该消除在onDraw()方法里的分配,因为这个分配会成为一个垃圾收集站,让你的view变得拖沓。把对象都放在初始化的位置,或者在过渡动画之间,永远不要再动画运行的时候分配。

为了让onDraw()更加精干,越少调用它越好。大多数对onDraw()方法的调用时调用了invalidate(),所以我们应该消除invalidate()的不必要调用。

另一个很大的开销就是在布局文件里消耗的时间。任何时候调用requestLayout(),Android U都必须要历遍一遍整个View的层次结构看看每个VIEW需要多大的尺寸。如果它发现冲突,还要重复多次历遍。UI设计师有时需要创建更深的嵌套结构去让UI表现的更好。这些深度的View结构会造成性能问题。让你的View层次结构越浅越好。

如果你有一个复杂的UI,考虑写一个自定义的ViewGroup去当它的布局。和内置的View不同,你自定义的View能,你自定义的View会对自己里面的子元素的大小和形状作出假设,从而避免了深度历遍的麻烦。这个PieChart展示了如何拓展ViewGroup作为自定义View的一部分,但是它从来没有测量它。而是它会根据自己自定义的布局算法去直接设置好他们的尺寸。

翻译死了!好了,这篇内容就这么多了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  谷歌