Android自定义view的一般步骤

Android原生已经提供了一系列控件,如TextView,EditText等,但是这些往往不能满足应用开发需求。当现有的控件满足开发需求的时候,就需要按照自己的需求自定义控件。自定义控件主要有以下几点:
1.继承自View或某个widget 2.定义属性及style 3.应用属性 4.定义属性事件方法 5.View的测量、布局及绘制 6.控件优化 7.其他细节问题
继承自View或某个widget
Android framework里面控件都是从View派生出来的。你自定义的view也可以直接继承View,或者你可以通过继承既有的一个子类(例如Button)来节约一点时间。 为了能够在layout xml文件中添加你自定义的控件,并使Android framework能够inflage自定义控件,你必须至少提供一个constructor,它包含一个Contenx与一个AttributeSet对象作为参数。

  public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
    }

为什么一定要实现这个constructor? framwork 中inflate layout文件方法其实就是去解析xml文件,递归找到每一个TAG对应的类,反射获取对应类中带有参数Context和AttributeSet的构造函数,再通过这个构造函数去创建一个实例。所以如果你的控件中不包含这个constructor就会报错,在layout xml中使用自定义控件一定要把包名类名写完整。
定义属性及style
如果你的控件是继承自View,那View相关的属性可以一并继承过来,比如background,View已经有了这个属性,某些情况下涉及绘制相关的,你可以直接在xml指定控件背景,但是一些新增行为或样式,就需要通过自定义属性实现。 属性一般放置于res/values/attrs.xml文件中,下面是一个attrs.xml文件的示例:

<resources>
    <declare-styleable name="CustomView">
        <attr name="mCircleRadius" format="dimension" />
        <attr name="mCircleColor" format="reference|color" />
    </declare-styleable>
</resources>

上面的代码定义了两个属性,mCircleRadius与mCircleColor,它们都归属于CustomView这个styleable。styleable的名字,没有严格的要求,通常与自定义的控件名字一致。 mCircleRadius对应的格式是dimensions, mCircleColor则可以是一个具体的颜色值或引用。
定义了属性之后,就可以在layout xml文件中使用它们,具体用法跟使用原生属性一样,唯一不同的是自定义属性是归属于不同的命名空间。不是属于[http://schemas.android.com/apk/res/android的命名空间,重定义命名控件http://schemas.android.com/apk/res/http://schemas.android.com/apk/res/%5byour“>your package name] 或 http://schemas.android.com/apk/res-auto,实例如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:my="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <com.example.customviewdemo.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dip"
        android:id="@+id/custom_view"
        flyme:mCircleRadius="10dip"
        flyme:mCircleColor="@android:color/darker_gray"
        />
</RelativeLayout>

需要注意:如果自定义的控件是一个内部类,比如CustomView的一个内部类InnerCustomView,则需要改成com.example.customviewdemo.CustomView$InnerCustomView
有时候同一个控件可能在多个地方用,如果每个地方都需要设置相同属性,那就会有很多重复的工作,一旦需要修改,需要一个个去修改。可以通过style解决这个问题。 在styles.xml文件中定义一个style

<style name="CustomView" >
     <item name="mCircleColor">#eeee00</item>
     <item name="mCircleRadius">50dip</item>
</style>

layout xml文件中就可以用style替换之前设置属性

<com.example.customviewdemo.CustomView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:padding="10dip"
     android:id="@+id/custom_view"
     style="@style/CustomView"
/>

为了方便所有使用同一Theme主题的控件都能有统一的风格,可以将sytle添加到Theme主题中。 定义一个主题属性,注意styleable命名不要用Theme

<declare-styleable name="MyTheme">
    <attr name="CustomViewStyle" format="reference"></attr>
</declare-styleable>

自定义主题,为CustomViewStyle属性指定一个style。

<resources>
    <!--
        Base application theme, dependent on API level. This theme is replaced
        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
    -->
    <style name="AppTheme" parent="Theme.AppCompat.Light">
          <item name="CustomViewStyle">@style/CustomView</item>
    </style>

        <style name="CustomView" >
       <item name="mCircleColor">#eeee00</item>
       <item name="mCircleRadius">50dip</item>
    </style>
</resources>

在AndroidManifest.xml中应用自定义Theme


<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

应用属性
当控件从layout xml被创建的时候,在xml标签下的属性值都是从resource下读取出来并传递到控件的constructor方法作为一个AttributeSet参数,然后通过Context方法obtainStyledAttributes()来获取属性值。这个方法会返回一个TypedArray对象,TypedArray可以看做一个属性集合,接着就可以从TypedArray中获取自定义属性的值。

public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, 0);
        mCircleColor = a.getColor(R.styleable.CustomView_mCircleColor, Color.BLACK);
        mCircleRadiu = a.getDimensionPixelSize(R.styleable.CustomView_mCircleRadius,0);
        a.recycle();
    }

需要注意:TypedArray对象是一个共享资源,必须被在使用后进行回收。
添加属性事件方法
上面自定义的属性只能在xml中使用,但仅仅能够在view被初始化的时候被读取到。每一个属性基本都需要对应一个动态方法,所以还需要提供一系列的 getter与setter方法。

public int getCircleColor() {
        return mCircleColor;
    }
  public int getCircleRadiu() {
        return mCircleRadiu;
    }
   public void setCircleRadiu(int radiu){
        if(radiu != mCircleRadiu){
            mCircleRadiu = radiu;
            requestLayout();
        }
    }
public void setCircleColor(int color){
        if(color != mCircleColor){
            mCircleColor = color;
            invalidate();
        }

需要注意:在setCircleColor方法里面有调用invalidate()触发控件重绘. 当控件的某些内容发生变化的时候,需要调用invalidate来通知系统对这个view进行重绘,在setCircleRadiu方法里面调用requestLayout();mCircleRadiu变化会引起组件大小变化时,需要调用requestLayout方法。调用invalidate()和requestLayout()之后系统并不是立刻就下执行,而是将需要重绘或者重新layout的请求传递到ViewRootImpl, 等待系统每隔16ms发出VSYNC信号(涉及Android Display相关),触发UI重新渲染,VSYNC信号到达之后回调ViewRootImpl类中TraversalRunnable这个callback去执行doTraversal方法,从而对当前窗口控件真正去执行layout或invalidate事件。必须确保每次渲染都能在16ms内完成,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps。所以我们需要尽可能让控件变得简单一些,如果每次渲染都会执行一些复杂的layout及绘制工作不仅仅加重CPU的负载,也会让界面变得卡顿,什么时候需要invalidate(),什么时候需要requestLayout()要区分清楚。
另外一点需要注意的是,不要在子线程中更新UI界面,比如调用setCircleRadius或setCircleColor,ViewRootImpl类中每次对View进行layout或重绘,都会检查是否主线程调用,如果你的应用在子线程中更新了View没有报错,可能两方面原因,你的View还有没有附加到窗口上,或者你的Activity已经处于stop状态,反正这两种情况下,你的请求都不会执行。子线程触发UI刷新可以用postIvalidate,postIvalidate本质上也是通过handler通知UI线程去执行。
除了基本的属性事件,有些情况下还需要支持响应事件的监听,例如点击、长按响应等。

View的测量、布局及绘制
了解measure、layout、draw的作用
measure
测量控件自身及子控件大小,measure过程中自上而下遍历所有子控件,最终每个控件测量得到的结果保存到mMeasuredWidth 和 mMeasuredHeight这两个变量中,可以通过getMeasuredWidth()和getMeasuredHeight()方法获得。
@Override

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int desiredWidth = mCircleRadiu * 2 + getPaddingLeft() +getPaddingRight();
        int desiredHeight = mCircleRadiu * 2 + getPaddingTop() +getPaddingBottom();
        int widthSize = resolveAdjustedSize(desiredWidth, getMinWidth(), widthMeasureSpec);
        int heightSize =  resolveAdjustedSize(desiredHeight, getMinHeight(), heightMeasureSpec);
        setMeasuredDimension(widthSize, heightSize);
    }

layout

布局子控件的位置,例如LinearLayout中决定了子控件横向或纵向排布。子控件的具体位置都是相对于父容器而言的。 在 layout 过程中,控件会调用getMeasuredWidth()和getMeasuredHeight()方法获取到 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight,作为自己的 width 和 height。然后调用每一个子视图的layout(l, t, r, b)函数,来确定每个子视图在父视图中的位置。
draw
绘制控件外观,如背景,分割线,滚动条等等。

 @Override
    protected void onDraw(Canvas canvas) {
        // TODO Auto-generated method stub
        super.onDraw(canvas);
        if(mCircleRadiu > 0){
            canvas.restore();
            Paint paint = new Paint();
            paint.setColor(mCircleColor);
            canvas.drawCircle(getWidth()/2, getHeight()/2, mCircleRadiu, paint);
            canvas.save();
        }
    }

控件优化

1、不要在初始化方法中执行耗时操作,是否有必要加载资源,加载布局文件等等。 2、不要随意调用invalidate、requestLayout,局部刷新的情况下可调用 invalidate(Rect dirty)或invalidate(int l, int t, int r, int b)。 3、过度绘制,减少布局层次,减少没必要的背景。某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。 4、适当情况打开硬件加速,充分利用GPU的特性,使得绘制更加平滑,但是会多消耗内存,个别绘制操作不支持导致控件显示异常等问题,具体见http://developer.android.com/guide/topics/graphics/hardware-accel.html。 5、尽可能不要在onDraw方法去加载bitmap,内存分配可能会触发GC,导致卡顿。
其他需要注意的问题
2,控件命名,控件名最好是体现功能特点 2、控件中使用api要确认需要兼容的版本,@SuppressLint(“NewApi”)和@TargetApi()仅仅只是屏蔽lint报错,在方法中还要判断版本做兼容处理。 3、某些情况下可以通过控件组合,根据业务逻辑增加一些方法控制,不一定每一个都需要重新定义一个新的控件 4、某些情况下涉及绘制相关的,可以通过定制drawable实现,而不一定要定制控件,例如demo的CustomView,只是重新处理了绘制相关内容,完全可以定义一个drawable代替。这样对应用使用方便,扩展性更高。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值