android自定义view

前言

网上有很多自定义View的说明的文章,首先通过以下问题,测试自己是否掌握自定义View:

1、Google提出View这个概念的目的是什么?

2、View这个概念与Activtiy、Fragment以及Drawable之间是一种什么样的关系?

3、View能够感知Activity的生命周期事件吗?为什么?

4、View的生命周期是什么?

5、当View所在的Activity进入stop状态后,View去哪了?如果我在一个后台线程中持有一个View的引用,我此时能够改变它的状态吗?为什么?

6、View能够与其他的View交叉重叠吗?重叠区域发生的点击事件交给谁去处理呢?可不可以重叠的两个View都处理?

7、View控制一个Drawable的方法途径有哪些?Drawable能不能与View通信?如果能如何通信?

8、假如View所在的ViewGroup中的子View减少了,View因此获得了更大的空间,View如何及时有效地利用这些空间,改变自己的绘制?

9、假如我要在View中动态地注册与解除广播接收器,应该在哪里完成呢?

10、假如我的手机带键盘(自带或者外接),你的自定义View应该如何响应键盘事件。

11、AnimationDrawable作为View的背景,会自动进行动画,View在其中扮演了怎样的角色?

假如以上问题你都能准确地回答出来,那你的自定义View已经学到家了。

好了,说了这么多,到底怎样才能学好自定义View?其实只需掌握三个问题,就可以轻松搞定它:

问题一:从Android系统设计者的角度,View这个概念究竟是做什么的?

问题二:Android系统中那个View类,它有哪些默认功能和行为,能干什么,不能干什么?(知己知彼,才好自定义!)

问题三:我要改变这个View的行为,外观,肯定是覆写View类中的方法,但是怎么覆写,覆写哪些方法能够改变哪些行为?

View的说明

看官方文档给出的对于View的描述

能够看出几点:1、view是用户界面的基础构建区块。2、view在屏幕上占一个矩形区域,用来绘制以及事件响应。

View.class的功能,行为

看几个问题,带着问题去思考学习:

View是怎样被显示到屏幕上的?

View在屏幕上的位置是怎样决定的?

View所占据的矩形大小是怎样决定的?

屏幕上肯定不止一个View,View之间互相知道对方吗?它们之间能协作吗?



一个UI上有很多view,有基础view,也有复合view。所有的view是怎么组合起来的呢?Google是这么解决的:用Window来展示用户界面,Window加载一个DecorView,用DecorView来包含所有的其他的view。

1、确定view的位置

我们在activity中setContentView,实际上就是将用户界面的所有的View交给了DecorView中的一个FrameLayout,这个FrameLayou代表着可以分配给用户界面使用的屏幕区域。更常见的情况是,用户界面是一个ViewGroup,里面包含了其他ViewGroup和View。

开发者在使用view的时候,向ViewGroup说明想把view放在什么位置,以LinearLayout,vertical为例,在写布局文件时,子View在LinearLayout中的出现顺序将决定它们在屏幕上的上下顺序,同时还可以借助layout_margin ,layout_gravity等配置进一步调整子View在分给自己的矩形区域中的位置。layout_*虽然是跟view的属性写在一起,但是其实并不是view的属性。这些值在Inflate时,是由ViewGroup读取,然后生成一个ViewGroup特定的LayoutParams对象,再把这个对象存入子View中的,这样,ViewGroup在为该子View安排位置时,就可以参考这个LayoutParams中的信息了。

2、确定View大小

第一步,开发者在书写布局文件时,会为一个View写上android:layout_width="***"android:layout_height="***"两个配置,这是开发者向ViewGroup表达的,我这个View需要的大小是多少。星号的取值有三种:

具体值,如50dp,很简单,不多讲

match_parent ,表示开发者向ViewGroup说,把你所有的屏幕区域都给这个View吧。

wrap_parent,表示开发者向ViewGroup说,只要给这个View够他展示自己的空间就行,至于到底给多少,你直接跟View沟通吧,看它怎么说。

第二步,ViewGroup收到了开发者对View大小的说明,然后ViewGroup会综合考虑自己的空间大小以及开发者的请求,然后生成两个MeasureSpec对象(width与height)传给View,这两个对象是ViewGroup向子View提出的要求。然后,这两个对象将会传到子View的protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中。子View再看ViewGroup的要求,于是,它从传入的两个对象中解译出如下信息:

Mode与Size一起,准确表达出了ViewGroup的要求。

Mode的取值有三种,它们代表了ViewGroup的要求条件:

1、MeasureSpec.EXACTLY,精确模式

在这种模式下,尺寸的值是多少,那么这个组件的长或宽就是多少。

2、MeasureSpec.AT_MOST,最大模式

这个也就是父组件,能够给出的最大的空间,当前组件的长或宽最大只能为这么大,当然也可以比这个小。

3、MeasureSpec.UNSPECIFIED,未指定模式

这个就是说,当前组件,可以随便用空间,不受限制。

如果子view不想遵守ViewGroup的要求怎么办?就是子view一定要设置自己考虑后的尺寸,如果不设置就相当于没有告诉ViewGroup自己想要的大小,这会导致ViewGroup无法正常工作,设置的办法就是在onMeasure方法的最后,调用setMeasuredDimension方法。


3、view的绘制

主要就是onDraw方法,主要执行几步:

绘制背景;

通过onDraw()绘制自身内容;

通过dispatchDraw()绘制子View;

绘制滚动条

4、改变view的行为,显示外观

肯定是要重载View.class中的方法,来看官方怎么说的



从上可以看出view的生命周期。View被inflated出来后,系统会回调该View的onFinishInflate方法。其他方法可以自己看文档。

5、实战

代码可见:https://github.com/feb07/CustomView

public class CustomView extends View {

    private Time time;

    //图片

    private Drawable bgDrawable;

    private Drawable hourDrawable;

    private Drawable minDrawable;

    //尺寸

    private int bgWidth;

    private int bgHeight;

    private boolean mAttached;

    //看名字

    private float mMinutes;

    private float mHour;

    //用来跟踪我们的View 的尺寸的变化,

//当发生尺寸变化时,我们在绘制自己

//时要进行适当的缩放。

    private boolean mChanged;

    public CustomView(Context context) {

        this(context, null);

    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {

        this(context, attrs, 0);

    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        this(context, attrs, defStyleAttr, 0);

    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {

        super(context, attrs, defStyleAttr, defStyleRes);

        //初始化图片信息

        if (bgDrawable == null) {

            bgDrawable = context.getDrawable(R.drawable.bg);

        }

        if (hourDrawable == null) {

            hourDrawable = context.getDrawable(R.drawable.hour);

        }

        if (minDrawable == null) {

            minDrawable =

                    context.getDrawable(R.drawable.min);

        }

        time = new Time();

        bgWidth = bgDrawable.getIntrinsicWidth();

        bgHeight = bgDrawable.getIntrinsicHeight();

    }

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        float hScale = 1.0f;

        float vScale = 1.0f;

        if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < bgWidth) {

            hScale = (float) widthSize / (float) bgWidth;

        }

        if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < bgHeight) {

            vScale = (float) heightSize / (float) bgHeight;

        }

        float scale = Math.min(hScale, vScale);

        setMeasuredDimension(

                resolveSizeAndState((int) (bgWidth * scale), widthMeasureSpec, 0),

                resolveSizeAndState((int) (bgHeight * scale), heightMeasureSpec, 0)

        );

    }

    @Override

    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        super.onSizeChanged(w, h, oldw, oldh);

        mChanged = true;

    }

    private void onTimeChanged() {

        time.setToNow();

        int hour = time.hour;

        int minute = time.minute;

        int second = time.second;

        /*这里我们为什么不直接把minute设置给mMinutes,而是要加上

            second /60.0f呢,这个值不是应该一直为0吗?

            这里又涉及到Calendar的 一个知识点,

            也就是它可以是Linient模式,

            此模式下,second和minute是可能超过60和24的,具体这里就不展开了,

            如果不是很清楚,建议看看Google的官方文档中讲Calendar的部分*/

        mMinutes = minute + second / 60.0f;

        mHour = hour + mMinutes / 60.0f;

        mChanged = true;

    }

    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {

        @Override

        public void onReceive(Context context, Intent intent) {

            //这个if判断主要是用来在时区发生变化时,更新mCalendar的时区的,这

            //样,我们的自定义View在全球都可以使用了。

            if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {

                String tz = intent.getStringExtra("time-zone");

                time = new Time(TimeZone.getTimeZone(tz).getID());

            }

            //进行时间的更新

            onTimeChanged();

            //invalidate当然是用来引发重绘了。

            invalidate();

        }

    };

    @Override

    protected void onAttachedToWindow() {

        super.onAttachedToWindow();

        if (!mAttached) {

            mAttached = true;

            IntentFilter filter = new IntentFilter();

            //这里确定我们要监听的三种系统广播

            filter.addAction(Intent.ACTION_TIME_TICK);

            filter.addAction(Intent.ACTION_TIME_CHANGED);

            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);

            getContext().registerReceiver(mIntentReceiver, filter);

        }

        time = new Time();

        onTimeChanged();

    }

    @Override

    protected void onDetachedFromWindow() {

        super.onDetachedFromWindow();

        if (mAttached) {

            getContext().unregisterReceiver(mIntentReceiver);

            mAttached = false;

        }

    }

    @Override

    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        //View尺寸变化后,我们用changed变量记录下来,

        //同时,恢复mChanged为false,以便继续监听View的尺寸变化。

        boolean changed = mChanged;

        if (changed) {

            mChanged = false;

        }

        int availableWidth = getRight() - getLeft();

        int availableHeight = getBottom() - getTop();

        int x = availableWidth / 2;

        int y = availableHeight / 2;

//画背景

        final Drawable drawable = bgDrawable;

        int w = drawable.getIntrinsicWidth();

        int h = drawable.getIntrinsicHeight();

        boolean scaled = false;

        /*如果可用的宽高小于表盘图片的宽高,

          就要进行缩放,不过这里,我们是通过坐标系的缩放来实现的。

          而且,这个缩放效果影响是全局的,

          也就是下面绘制的表盘、时针、分针都会受到缩放的影响。*/

        if (availableWidth < w || availableHeight < h) {

            scaled = true;

            float scale = Math.min((float) availableWidth / (float) w,

                    (float) availableHeight / (float) h);

            canvas.save();

            canvas.scale(scale, scale, x, y);

        }

        /*如果尺寸发生变化,我们要重新为表盘设置Bounds。

          这里的Bounds就相当于是为Drawable在View中确定位置,

          只是确定的方式更直接,直接在View中框出一个与Drawable大小

          相同的矩形,

          Drawable就在这个矩形里绘制自己。

          这里框出的矩形,是以(x,y)为中心的,宽高等于表盘图片的宽高的一个矩形,

          不用担心表盘图片太大绘制不完整,

            因为我们已经提前进行了缩放了。*/

        if (changed) {

            drawable.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));

        }

        drawable.draw(canvas);

        canvas.save();

//接下来画时针

        /*根据小时数,以点(x,y)为中心旋转坐标系。

            */

        canvas.rotate(mHour / 12.0f * 360.0f, x, y);

        final Drawable hourHand = hourDrawable;

        //同样,根据变化重新设置时针的Bounds

        if (changed) {

            w = hourHand.getIntrinsicWidth();

            h = hourHand.getIntrinsicHeight();

            /* 仔细体会这里设置的Bounds,我们所画出的矩形,

                同样是以(x,y)为中心的

                矩形,时针图片放入该矩形后,时针的根部刚好在点(x,y)处,

                因为我们之前做时针图片时,

                已经让图片中的时针根部在图片的中心位置了,

                虽然,看起来浪费了一部分图片空间(就是时针下半部分是空白的),

                但却换来了建模的简单性,还是很值的。*/

            hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));

        }

        hourHand.draw(canvas);

        canvas.restore();

        canvas.save();

        //根据分针旋转坐标系

        canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);

        final Drawable minuteHand = minDrawable;

        if (changed) {

            w = minuteHand.getIntrinsicWidth();

            h = minuteHand.getIntrinsicHeight();

            minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));

        }

        minuteHand.draw(canvas);

        canvas.restore();

        //最后,我们把缩放的坐标系复原。

        if (scaled) {

            canvas.restore();

        }

    }

}

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

推荐阅读更多精彩内容