Android自定义View

自定义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 在<declare-stylable>资源元素中定义我们的自定义属性###

res/values/attrs.xml

    <resources>
       <declare-styleable name="MyView">
           <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,属于MyView.这里的name最好跟我们自定义的View名字相同,不过这不是强制的,但是正常开发中一般都这么做。

完成上面的xml后,我们就可以在layout中给这些属性赋值。就像Android提供的原生属性一样,唯一不同的是我们的自定义属性属于另外一个namespace.

http://schemas.android.com/apk/res/com.gome.farmpatner

默认的命名空间是

http://schemas.android.com/apk/res/android

根据上面的例子,在layout文件中,可以这样定义属性值

<?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.gome.farmpatner">
 <com.gome.farmpatner.MyView
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>

注意到这里,对于自定义控件我们引用的是全包名。
如果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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容