Android (非常重要)如何实现自定义的View

一个设计的好的自定义view,有着丰富的特性和简单的接口。它可以有效的使用cpu和内存。

所以一个自定义view必须满足以下几点:

  1. 符合Android的规范
  2. 提供可以和Android XML 布局相适应的 自定义的 属性
  3. 可以发送访问事件
  4. 兼容不同的android平台

android 框架提供了一些基本的类和XMl标签来帮助你创建一个合适的自定义view, 那么首先

如何创建一个View的类 (Class)

第一步,继承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 文件中

  • 在resource元素的中,为你的view自定义属性
  • 特别指明你的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>

这个代码有两个 属性, showTextlabelPosition 属于一个 名字为 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上


自定义绘制 Custom Drawing

自定义控件最重要的部分大概就是 外观了,根据你的app需求,自定义 绘制 可以很简单也可以很复杂。继续了解吧!

重写 onDraw() 方法

绘制一个 自定义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变卡。 最好提前全部创建好

处理 布局事件 Layout Events (测绘)

为了绘制合适,在绘制以前你得知道 大小尺寸 是多少。复杂的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 的一样大,值得注意的几个部分:

  • 计算考虑到了 view 的 padding
  • resolveSizeAndState() 方法用来创造一个 final 的 宽 和 高 的 值将,将 view 期望的值 和 传入 onMeasure 方法的值比较,返回一个合适的
  • onMeasure() 方法是没有返回值的,但是强制性的 调用 方法setMeasureDimension() 来传递结果

绘制!

一旦绘图对象创建好了,measure也定义好了。那么就可以实现 onDraw() 方法了。每一个view实现 onDraw() 方法都是不同的,但是有一些相同的操作

  • 绘制文本使用 DrawtText() ,设置字体使用 setTypeface(),设置字体颜色使用 setColor()
  • 绘制一些特定的图形,如 drawRect() drawArc() 等,填充和外边都是调用 setStyle 设置
  • 更加复杂的形状可以用 drawPath() 绘制
  • 通过创建一个 LinearGradient 对象来控制 渐变的填充,调用 setShader() 方法来使用你的 LinearGradient 填充形状
  • 绘制bitmap 使用 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);
}

创建view的接口

绘制出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 可以让实现一些物理的效果 比如滑动

几种优化view的思路:

  1. 不要在onDraw() 方法和 动画running的时候分配内存,这样垃圾回收会造成卡顿
  2. 尽量不要调用 onDraw() 方法,大部分对 onDraw() 调用都是 invalidate() ,所以尽量不使用 invalidate() 方法
  3. 遍历 layout 开销也是巨大的,requestLayout() 的每次调用,Android UI 系统都要遍历整个 view 层来找到 整个view所需的 空间大小。如果产生冲突,可能还得多次遍历。所以尽可能的让你的UI分布扁平
  4. 如果你是一个复杂的UI,那么可以考虑 用 ViewGroup 来表示其布局

你可能感兴趣的:(android学习)