Custom View


Creating a View Class

本处参考Android官方文档Creating Custom Views。为了能够设计出良好的自定义视图,需要遵守以下规则:

  • 遵从Android标准。
  • 提供自定义的styleable属性以便于在Android Xml中配置视图时使用。
  • Send accessibility events。
  • 兼容多个Android平台。
  • Android平台提供了许多基础类和xml标签来帮助我们创建满足这些要求的view。接下来讨论如何自定义拥有核心功能的view。

Android平台提供了许多基础类和xml标签来帮助我们创建满足要求的view。接下来讨论如何自定义拥有核心功能的view。


Subclass a View



所有Android内建的视图都是继承自 View。自己自定义的视图可以直接继承自View,也可以为了节省时间,继承自已经存在的View的子视图,如 Button。
为了使Android Studio能够识别自定义View,我们必须提供一个至少拥有两个参数的构造器,一个参数是 Context,另一个是 AttributeSet。这个构造器能够使工具识别自定义View,在layout编辑器里布局时,便可以创建和修改。

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



Define Custom Attributes

添加一个内建的View作为用户交互界面,是使用XML元素对其进行声明,并且通过元素属性控制其外观样式和行为动作。写得好的自定义视图也应该是可以通过XML进行添加和样式控制。要让自定义view能拥有这些能力,我们必须:

  • 在<declare-styleable>中为view自定义属性
  • 为XML layout中使用的属性自定义属性值
  • 在运行时获取属性值
  • 为自定义view设置上获取到的属性值
那么如何定义属性呢?
通常是将属性值设置在项目的res/values/attrs.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的属性。依据惯例,styleable实例的名字与自定义的View的类名一致。尽管不一定非要按照这样的习惯来写,但是许多编辑器是根据这样的命名习惯来提供代码提示的。


这要定义后就能够在xml layout文件中使用了。唯一不同的是自定义属性所从属的命名空间(namespace)不是http://schemas.android.com/apk/res/android,而是http://schemas.android.com/apk/res/你的包名。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/com.example.android.customviews"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

  <com.example.customviews.charting.PieChart
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      custom:showText="true"
      custom:labelPosition="left"
      />
</LinearLayout>


为了不一直重复长长的命名空间,便使用xmlns为http://schemas.android.com/apk/com.example.android.customviews指明了一个简单的别名,别名可以随便取。
添加到layout xml中的自定义视图需要使用全路径名。如果是内部类,还要指明外部类。举例:PieChart有一个内部类PieView,要这样使用PieView:com.example.customviews.charting.PieChart$PieView。


Apply Custom Attributes

自定义视图被定义在xml文件里后,我们所指定的属性会从resource bundle(资源包)中读入,并且以AttributeSet传递给该view的构造器。可以直接从AttributeSet中读取属性值,但是有许多不利因素:

  • Resource references within attribute values are not resolved
  • Styles are not applied
作为替代的,传递AttributeSet到obtainStyledAttributes()方法,该方法返回一个包含所使用的属性的 TypedArray。Android资源编译器做了许多工作来方便我们调用obtainStyledAttributes()方法。编译器会在R.java文件中为每一组资源文件中的中的属性生成id,也会为其中的属性定义常量。我们使用这些预先定义好的常量从TypedArray中获取属性。

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是共享资源,用了之后必须要回收。

Add Properties and Events

属性是控制行为和外观的非常强大的一种方式,但是它们只能在view被初始化时才能读取。为了能够提供动态的控制,可以暴露属性的getter和setter方法。


public boolean isShowText() {
  return mShowText;
}

public void setShowText(boolean showText) {
  mShowText = showText;
  invalidate();
  requestLayout();
}
setShowText方法调用了invalidate()和requestLayout()方法。这是保证view行为正常的重要步骤。必须在改变了可能引起view外观改变的属性后使view无效(也就是调用invalidate()方法),以便于系统知道该view需要重绘了。同样地,在改变了可能引起view尺寸或者形状改变的时候,也需要请求一个新的布局(requestLayout()方法)。忘记了调用这些方法,可能会出现很难查找的bugs。

自定义view也需要提供事件监听来传达重要的事件。PieChart提供了一个叫做OnCurrentItemChanged的事件来通知用户将饼图旋转到了新的扇形区(这里的饼图是一格一格的扇形嘛)。

特别是在只有自己才用的自定义视图时,很容易就忘记了提供属性设置方法和事件监听。花一点点时间注意下自定义视图的接口暴露问题可以减少后期的维护成本。总是提供能够影响视图展示和行为的属性的接口是一个很好的原则。


Design For Accessibility

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:

  • 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
For more information on creating accessible views, see Making Applications Accessible in the Android Developers Guide.

Custom Drawing


Override onDraw()

最为重要的便是实现onDraw()方法。该方法有一个Canvas参数,用于自定义视图绘制自己。Canvas类也定义了诸如文字、线、图片等等基本图形的绘制。在绘制图形前,需要先创建Paint对象。

Create Drawing Objects


android.graphics将绘制分为了两个部分:

  • 要绘制什么,由Canvas决定
  • 怎么绘制,由Paint决定
举个例子,Canvas提供了绘制线的方法,而Paint提供了方法来设置线的颜色。Canvas提供了绘制矩形的方法,而Paint提供了方法来设置是否使用某种颜色对其填充。简而言之,Canvas定义了能够在屏幕上绘制什么样的图像,Paint定义了所绘制图像的颜色、风格、字体。
在绘制任何东西之前,都需要先创建一个或者多个Paint,PieChart在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));

...

提前创建好对象是非常重要的一种优化方式。视图绘制非常频繁,许多绘制对象的创建需要耗费很多资源。在onDraw()方法里创建这些对象会大大降低性能,甚至界面绘制不流畅。

Handle Layout Events

要知道view的大小,以便于更好的绘制。复杂的自定义视图会根据自己在屏幕中的形状和大小执行多次布局计算。我们不能够假定视图在屏幕上的大小。即便只有一个应用使用我们的自定义试图,但是也要适配不同屏幕尺寸,多种屏幕密度,以及竖屏和横屏之间的切换。
不过,View有许多方法来进行测量,并且大多数都是不用重写的。若不需要特别的控制其大小,直接继承onSizeChanged()就可以了。当视图被赋予大小之后便会调用onSizeChanged()方法,当然,后来因为其他原因改变了大小,也会重新调用onSizeChanged()方法。计算出来的位置、尺寸以及与其大小相关的值都在onSizeChanged()方法里,便不用在绘制的时候每次都去重新计算了。在PieChart例子中,onSizeChanged()方法会在计算出饼图的边框、文本和其他可见元素的相对位置时回调。
为视图指定大小时,布局管理器会假设这个大小中包括了视图内部的边距(padding)的。我们必须自己处理padding。下面是PieChart中的onSizeChanged()片段:

@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  // 统计padding
  float xpad = (float) (getPaddingLeft() + getPaddingRight());
  float ypad = (float) (getPaddingTop() + getPaddingBottom());

  // 加上文本的宽度
  if (mShowText) xpad += mTextWidth;

  float ww = (float) w - xpad;
  float hh = (float) h - ypad;
  
  // 计算出饼图的大小
  float diameter = Math.min(ww, hh);
}

实现onMeasure()方法来更好的控制布局参数(layout parameters)。该方法的参数View.MeasureSpec中包含了父控件想要子视图绘制的大小,并且这个大小是要么是最大值,要么仅仅是一个建议。为了优化,这些值是以包装好了的形式存放,应当使用View.MeasureSpec来获取每个整形中的信息。


In this implementation, PieChart attempts to make its area big enough to make the pie as big as its label:

@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的内边距,这是其职责。
  • resolveSizeAndState()方法用来获得最终的宽度和高度,该方法通过对自定义view所期望的值与传递进 onMeasure()的spec进行比较,从而返回合适的View.MeasureSpec值。
  • onMeasure() 没有返回值,而是将值传递给setMeasuredDimension()方法。该方法必须被调用,如果漏掉了会出现运行时异常。

Draw!


需要使用的对象和测量尺寸的代码编写完后,就可以实现onDraw()方法了。每个view实现onDraw()的方式都不同,却也有一些共通的特点:
  • 绘制文字使用drawText()方法。使用setTypeface()设置字体样式,setColor()方法设置字的颜色。
  • 绘制基本视图使用drawRect()(矩形), drawOval()(椭圆), and drawArc()(弧线)。setStyle()方法设置是否填充、边框以及两者都有。
  • 可以使用Path绘制更加复杂的图形。通过Path定义好线和曲线的路径,然后使用drawPath()进行绘制。对于基本图形,路径可以是外边框,填充,或者两者都有,都可以通过setStyle()设置。
  • Define gradient fills by creating LinearGradient objects. Call setShader() to use your LinearGradient on filled shapes.
  • 创建LinearGradient来实现j填渐进式充。调用setShader()来使LinearGradient填充图形。
  • 使用drawBitmap()方法来绘制bitmaps。
下面是绘制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);
}


Making the View Interactive

绘制仅仅只是自定义view的一部分。还必须使自定义view响应用户事件。对象的行为活动要跟真实世界里一样,比如图片展示出来后,然后又在其他地方出现了,真实世界里是不会出现这种情况的。正常的应该是图片从一个地方移动到另一个地方。

Users also sense subtle behavior or feel in an interface, and react best to subtleties that mimic the real world. For example, when users fling a UI object, they should sense friction at the beginning that delays the motion, and then at the end sense momentum that carries the motion beyond the fling.

这个小节示范如何使用Android体系结构的特征为自定义view添加真实世界的行为。


Handle Input Gestures

Android支持用户输入事件。用户的行为会被转换成事件且进行回调,我们可以重写该回调方法来响应用户。最为普通的便是触摸事件,触发onTouchEvent(android.view.MotionEvent)方法。重写该方法来处理事件:
   @Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

系统处理的触摸事件不怎么有用。根据手势定义了一些流行的触摸事件,比如tapping, pulling(下拉), pushing(上拉), flinging(估计是用力拉,一次滑很远), zooming(缩放)。Android提供了GestureDetector类将原始的触摸事件转换为手势。


初始化GestureDetector需要传递实现GestureDetector.OnGestureListener的类。若是想要自己处理手势流程,就继承GestureDetector.SimpleOnGestureListener。下面的代码便是继承自GestureDetector.SimpleOnGestureListener,并且实现其onDraw()方法。
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。This step is necessary because all gestures begin with an onDown() message.If you return false from onDown(), as GestureDetector.SimpleOnGestureListener does, the system assumes that you want to ignore the rest of the gesture, and the other methods of GestureDetector.OnGestureListener never get called. 唯一返回false的情况是真的想要忽略全部的手势。实现GestureDetector.OnGestureListener并创建了实例之后,就可以在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;
}

When you pass onTouchEvent() a touch event that it doesn't recognize as part of a gesture, it returns false. You can then run your own custom gesture-detection code.

Create Physically Plausible Motion

Gestures are a powerful way to control touchscreen devices, but they can be counterintuitive and difficult to remember unless they produce physically plausible results. A good example of this is the fling gesture, where the user quickly moves a finger across the screen and then lifts it. This gesture makes sense if the UI responds by moving quickly in the direction of the fling, then slowing down, as if the user had pushed on a flywheel and set it spinning.
对触屏设备来说手势是非常重要的,但是它们是不够直观的且难以记住的,除非是物理层做出了直观的反应。fling手势(快速滑动)是很好的例子,用户很快的滑动屏幕然后离开屏幕。这个手势的反应是界面沿着滑动方向迅速移动,然后慢慢停下来。

However, simulating the feel of a flywheel isn't trivial. A lot of physics and math are required to get a flywheel model working correctly. Fortunately, Android provides helper classes to simulate this and other behaviors. The Scroller class is the basis for handling flywheel-style fling gestures.
模仿飞轮的感觉并不重要。飞轮模式需要许多物理和数学知识。幸运的是,Android提供了帮助类来模拟这些手势。Scroller类是处理fling手势最基础的类。

To start a fling, call fling() with the starting velocity and the minimum and maximum x and y values of the fling. For the velocity value, you can use the value computed for you by 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();
}

Note: Although the velocity calculated by GestureDetector is physically accurate, many developers feel that using this value makes the fling animation too fast. It's common to divide the x and y velocity by a factor of 4 to 8.
Note:即便使用GestureDetector进行速度计算是物理精确,但是许多开发者觉得使用这样计算出来的值能使动画更流畅。

The call to fling() sets up the physics model for the fling gesture. Afterwards, you need to update the Scroller by calling Scroller.computeScrollOffset() at regular intervals. computeScrollOffset() updates the Scroller object's internal state by reading the current time and using the physics model to calculate the x and y position at that time. Call getCurrX() and getCurrY() to retrieve these values.

Most views pass the Scroller object's x and y position directly to scrollTo(). The PieChart example is a little different: it uses the current scroll y position to set the rotational angle of the chart.
if (!mScroller.isFinished()) {
    mScroller.computeScrollOffset();
    setPieRotation(mScroller.getCurrY());
}
The Scroller class computes scroll positions for you, but it does not automatically apply those positions to your view. It's your responsibility to make sure you get and apply new coordinates often enough to make the scrolling animation look smooth. There are two ways to do this:

  • Call postInvalidate() after calling fling(), in order to force a redraw. This technique requires that you compute scroll offsets in onDraw() and call postInvalidate() every time the scroll offset changes.
  • Set up a ValueAnimator to animate for the duration of the fling, and add a listener to process animation updates by calling addUpdateListener().
The PieChart example uses the second approach. This technique is slightly more complex to set up, but it works more closely with the animation system and doesn't require potentially unnecessary view invalidation. The drawback is that ValueAnimator is not available prior to API level 11, so this technique cannot be used on devices running Android versions lower than 3.0.
Note:  You can use  ValueAnimator  in applications that target lower API levels. You just need to make sure to check the current API level at runtime, and omit the calls to the view animation system if the current level is less than 11.
       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();
               }
           }
       });

Make Your Transitions Smooth


Users expect a modern UI to transition smoothly between states. UI elements fade in and out instead of appearing and disappearing. Motions begin and end smoothly instead of starting and stopping abruptly. The Android property animation framework, introduced in Android 3.0, makes smooth transitions easy.

To use the animation system, whenever a property changes that will affect your view's appearance, do not change the property directly. Instead, use ValueAnimator to make the change. In the following example, modifying the currently selected pie slice in PieChart causes the entire chart to rotate so that the selection pointer is centered in the selected slice. ValueAnimator changes the rotation over a period of several hundred milliseconds, rather than immediately setting the new rotation value.

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

If the value you want to change is one of the base View properties, doing the animation is even easier, because Views have a built-in ViewPropertyAnimator that is optimized for simultaneous animation of multiple properties. For example:

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


Optimizing the View

Now that you have a well-designed view that responds to gestures and transitions between states, ensure that the view runs fast. To avoid a UI that feels sluggish or stutters during playback, ensure that animations consistently run at 60 frames per second.


Do Less, Less Frequently


To speed up your view, eliminate unnecessary code from routines that are called frequently. Start by working ononDraw(), which will give you the biggest payback. In particular you should eliminate allocations in onDraw(), because allocations may lead to a garbage collection that would cause a stutter. Allocate objects during initialization, or between animations. Never make an allocation while an animation is running.

In addition to making onDraw() leaner, also make sure it's called as infrequently as possible. Most calls toonDraw() are the result of a call to invalidate(), so eliminate unnecessary calls to invalidate().

Another very expensive operation is traversing layouts. Any time a view calls requestLayout(), the Android UI system needs to traverse the entire view hierarchy to find out how big each view needs to be. If it finds conflicting measurements, it may need to traverse the hierarchy multiple times. UI designers sometimes create deep hierarchies of nested ViewGroup objects in order to get the UI to behave properly. These deep view hierarchies cause performance problems. Make your view hierarchies as shallow as possible.

If you have a complex UI, consider writing a custom ViewGroup to perform its layout. Unlike the built-in views, your custom view can make application-specific assumptions about the size and shape of its children, and thus avoid traversing its children to calculate measurements. The PieChart example shows how to extendViewGroup as part of a custom view. PieChart has child views, but it never measures them. Instead, it sets their sizes directly according to its own custom layout algorithm.




参考链接

Creating Custom Views







你可能感兴趣的:(android)