Android学习之创建自定义View(入门)

来源:互联网 发布:c编程思想pdf 编辑:程序博客网 时间:2024/06/05 07:00

自定义的view应该:

  • 遵守Android标准规则。
  • 提供自定义的风格属性值能够被Android XML Layout所识别
  • 发出可访问的事件
  • 能够兼容Android的不同平台

Android的framework提供了许多基类与XML标签用来创建一个符合上面要求的view。

Android framework里面定义的view类都继承自view。所以我们自己定义的view也可以直接继承view,或者可以通过继承现有的一个子类(如Button)来节约时间。

为了让Android developer tool 能够识别view,必须至少提供一个constructor,它包含一个context和attributeSet对象作为参数。这个constructor允许layout editor创建并编辑view实例

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

定义自定义属性

为了添加一个内置的View到UI上,需要通过XML属性来指定它的样式与行为。良好的自定义views可以通过XML添加和改变样式,为了让自定义view也有如此的行为,应该:

  • 在资源标签CIA定义自设的属性
  • 在XML layout中指定属性值
  • 在运行时获取属性值
  • 把获取到的属性值应用在view上

为了定义自设的属性,添加资源到我们的项目中,放置于res/values/atrrs.xml
文件中:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="PieChart">        <attr name="showText" format="boolean"/>        <attr name="lablePosition" format="enum">            <enum name="left" value="0"/>            <enum name="right" value="1"/>            </attr>    </declare-styleable></resources>

上面的代码声明了2个自设的属性,showText和lablePosition,它们都归属于PieChart的项目下的styleable实例。styleable实例的名字,通常与自定义的view的名字一致

一旦我们定义了自设的属性,我们就可以在layout/XML文件中使用它们,就像内置属性一样。唯一不同的是我们自设的属性是归属于不同的命名空间。不是属于http://schemas.android.com/apk/res/android的命名空间,它们归属于http://schemas.android.com/apk/res/[your package name]

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:custom="http://schemas.android.com/apk/com.example.asus1.testviews"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.example.asus1.testviews.MainActivity">   <com.example.asus1.testviews.PieChart       android:layout_width="match_parent"       android:layout_height="wrap_content"        custom:showText="true"       custom:lablePosition = "right"       /></LinearLayout>

为了避免输入长串的namespace名字,示例上面使用了xmlns指令,这个指令可以指派custom作为http://schemas.android.com/apk/com.example.asus1.testviewsnamespace的别名。我们也可以选择其他的别名作为我们的namespace

请注意,如果我们的view是一个inner class,我们必须指定这个view的outer class
比如,如果PieChart有一个inner class叫做PieView,为了使用这个类中自设的属性,我们应该使用http://schemas.android.com/apk/com.example.asus1.testview.PieChart$PieView

应用自定义属性

当view从XML layout被创建的时候,在xml标签下的属性值都是从resource下读取出来并传递到view的constructor作为一个AtrributeSet参数。尽管可以从AtrributeSet中直接读取数值,可是这样做有些弊端:

  • 拥有属性的资源并没有经过解析
  • styles并没有运用上

通过 attrs 的方法是可以直接获取到属性值的,但是不能确定值类型,如:
String title = attrs.getAttributeValue(null, “title”);
int resId =attrs.getAttributeResourceValue(null, “title”, 0);
title =context.getText(resId));
都能获取到 “title”属性,但你不知道值是字符串还是resId,处理起来就容易出问题,下面的方法则能在编译时就发现问题

取而代之的是,通过obtainStyledStrributes()来获取属性值。这个方法会船体一个TypedArray对象,它是间接referenced并且styled的

Android资源编译器帮我们做了许多工作来使调用obtainStyledStrributes()更简单。对res目录里每一个<declare-styleable>资源,自动生成的R.java文件定义了存放属性ID的数组和常量,常量用来索引数组中每个属性。我们可以使用这些预先定义的常量来从TypedArray中读取属性。

 public PieChart(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        TypedArray array = context.getTheme().obtainStyledAttributes(                attrs,                R.styleable.PieChart,                0,0);        try {           mShowText = array.getBoolean(R.styleable.PieChart_showText,false);            mTextPos = array.getInteger(R.styleable.PieChart_lablePosition,0);        }finally {            array.recycle();        }    }

请注意TypedArray对象是一个共享资源,必须被在使用后进行回收

添加属性和事件

Atrributes是一个强大的控制view的行为和外观的方法,但是他们仅仅能够在view 被初始化的时候被读取到。为了提供一个动态的行为,需要暴露出一些合适的getter与setter方法

public boolean isShowText() {   return mShowText;}public void setShowText(boolean showText) {   mShowText = showText;   invalidate();   requestLayout();}

在setShowText方法里面调用invalidate()和requestLayout(),这两个调用时确保稳定运行的关键。当view的某些内容发生变化的时候,需要调用invalidate来通知系统对这个view进行redraw,当某些元素变化会引起组件大小变化时,需要调用requestLayout方法。调用时如果忘了这两个方法,将会导致hard-to-find bugs

自定义的view也需要能够支持响应事件的监听器。例如,PieChart暴露了一个自定义的事件OnCurrentItemChanged来通知监听器,用户已经切换了焦点到一个新的组件上。

Override onDraw()

重绘一个自定义view的最重要的步骤是重写onDraw()方法。onDraw()的参数是一个Canvas对象。Canvas类定义了绘制文本,线条,图像与许多其他图形的方法。我们可以在onDraw方法里面使用那些方法来创建我们的UI

创建绘图对象

android.graphics framework把绘制定义为下面两类:

  • 绘制什么,由Canvas处理
  • 如何绘制,由Panit处理

例如Canvas提供绘制一条直线的方法,Paint提供直线的颜色。Cnavas提供绘制矩形的方法,Paint定义是否使用颜色填充。简单来说:Canvas定义我们再屏幕上画的图形,而Paint定义颜色,样式,字体

所以在绘制之前,我们需要创建一个或者多个Panit对象

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方法里面创建绘制对象会严重影响到性能并使得我们的UI显得卡顿

处理布局事件

为了正确的绘制我们的view,我们需要知道view的大小。复杂的自定义view通常需要根据在屏幕的大小与形状执行对此layout计算。而不是假设这个view在屏幕上的显示大小。即使只有一个程序会使用我们的view,仍然是需要处理屏幕大小不同,密度不同,方向不同所带来的影响

尽管view有许多方法是用来计算大小的,但是大多数是不需要重写的。如果我们的view不需要特别的控制它的大小,唯一需要重写的方法是onSizeChanged()

onSizeChanged(),当我们的view第一次被赋予一个大小时,或者我们的view大小被更改的时候,会被执行。在onSizeChanged方法里面计算位置,间距等其他与我们的view大小值

当我们的view被设置大小的时候,lauout manager(布局管理器)假定这个大小包括所有的view的内边距(padding)。但我们计算view大小的时候,我们必须处理内边距的值

  // 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的父控件的大小。那些值被包装成int类型,我们可以使用静态方法来获取其中的信息。

在这个例子中PieChart试着使它的区域足够大,使pie可以像它的label一样大:

@Overrideprotected 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的padding考虑进去
  • 帮助方法resolveSizeAndSate()是用来创建最终的宽高值的。这个方法比较view的期待值与传递给onMeasure的spec值,然后返回一个合适的View.MeasureSpec值
  • onMeasure方法没有返回值,它通过调用setMeasuresDimension()来获取结果。调用这个方法是强制执行的,如果遗漏这个方法,会出现运行时异常

绘图

每个view的onDraw都是不同的,但是有下面一些常见的操作:

  • 绘制文字使用drawText()。指定字体通过调用setTypedface(),通过setColor来设置文字颜色
  • 绘制基本图形使用drawRect(),drawOval(),drawArc()。通过setStyle()来指定形状是否需要filled,outlined。
  • 绘制一些复杂的图形,使用Path类,通过给Path对象添加直线和曲线,然后使用drawPath来绘制图形。和基本图形一样,也可以通过setStyle来设置
  • 通过创建LinearGradient对象来定义渐变。调用setShader()来使用LinearGradient
  • 通过使用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);}

实战

1。自定义View的属性,在res/values/下建立atrrs.xml

<?xml version="1.0" encoding="utf-8"?><resources>    <attr name="titleText" format="string"/>    <attr name="titleTextColor" format="color"/>    <attr name="titleTextSize" format="dimension"/>    <declare-styleable name="CustomTitleView">       <attr name="titleText"/>        <attr name="titleTextColor"/>        <attr name="titleTextSize"/>    </declare-styleable></resources>

然后创建我们自定义的view并在布局中声明它:

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:custom="http://schemas.android.com/apk/res/com.example.asus1.testviews"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.example.asus1.testviews.MainActivity">  <com.example.asus1.testviews.CustomTitleView      android:layout_width="200dp"      android:layout_height="100dp"       custom:titleText = "3712"      custom:titleTextColor = "#ff0000"      custom:titleTextSize = "40sp"      /></LinearLayout>

2。再view的构造方法中获得我们自定义的样式

 private String mTitleText;    private int mTitileTextColor;    private int mTitleTextSize;    private Rect mBound;    private Paint mPaint;    public CustomTitleView(Context context) {        this(context,null);    }    public CustomTitleView(Context context, @Nullable AttributeSet attrs) {        this(context, attrs,0);    }    public CustomTitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        TypedArray array = context.getTheme().obtainStyledAttributes(                attrs,R.styleable.CustomTitleView,defStyleAttr,0        );        int n = array.getIndexCount();        for(int i =0;i<n;i++){            int attr = array.getIndex(i);            switch (attr){                case R.styleable.CustomTitleView_titleText:                    mTitleText = array.getString(attr);                    break;                case R.styleable.CustomTitleView_titleTextColor:                    mTitileTextColor = array.getColor(attr, Color.BLACK);                    break;                case R.styleable.CustomTitleView_titleTextSize:                    mTitleTextSize = array.getDimensionPixelSize(attr,                            (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,getResources().getDisplayMetrics()));                    break;            }        }        array.recycle();        mPaint = new Paint();        mPaint.setTextSize(mTitleTextSize);        mPaint.setColor(mTitileTextColor);        mBound = new Rect();        mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);    }

3。重写onDraw,onMeasure

 @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    @Override    protected void onDraw(Canvas canvas) {        mPaint.setColor(Color.YELLOW);        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);        mPaint.setColor(mTitileTextColor);        canvas.drawText(mTitleText,getWidth()/2-mBound.width()/2,getHeight()/2+mBound.height()/2,mPaint);    }

这里写图片描述

是不是觉得还不错,基本已经实现了自定义View。但是此时如果我们把布局文件的宽和高写成wrap_content,会发现效果并不是我们的预期:

这里写图片描述

系统帮我们测量的高度和宽度都是match_parent,当我们设置明确的宽度和高度的时候,系统帮我们测量的结果就是我们设置的结果,当我们设置为wrap_content或者match_parent系统帮我们测量的结果就是match_parent的长度
所以当设置了wrap_content时,需要我们自己进行测量,即重写onMeasure方法:

重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用

@Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  {      int widthMode = MeasureSpec.getMode(widthMeasureSpec);      int widthSize = MeasureSpec.getSize(widthMeasureSpec);      int heightMode = MeasureSpec.getMode(heightMeasureSpec);      int heightSize = MeasureSpec.getSize(heightMeasureSpec);      int width;      int height ;      if (widthMode == MeasureSpec.EXACTLY)      {          width = widthSize;      } else      {          mPaint.setTextSize(mTitleTextSize);          mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);          float textWidth = mBounds.width();          int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());          width = desired;      }      if (heightMode == MeasureSpec.EXACTLY)      {          height = heightSize;      } else      {          mPaint.setTextSize(mTitleTextSize);          mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);          float textHeight = mBounds.height();          int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());          height = desired;      }      setMeasuredDimension(width, height);  }  

修改布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:tools="http://schemas.android.com/tools"      xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"      android:layout_width="match_parent"      android:layout_height="match_parent" >      <com.example.customview01.view.CustomTitleView          android:layout_width="wrap_content"          android:layout_height="wrap_content"          custom:titleText="3712"          android:padding="10dp"          custom:titleTextColor="#ff0000"          android:layout_centerInParent="true"          custom:titleTextSize="40sp" />  </RelativeLayout>  

这里写图片描述

3,添加事件
在构造中添加:

this.setOnClickListener(new OnClickListener()          {              @Override              public void onClick(View v)              {                  mTitleText = randomText();                  postInvalidate();              }          });  
private String randomText()      {          Random random = new Random();          Set<Integer> set = new HashSet<Integer>();          while (set.size() < 4)          {              int randomInt = random.nextInt(10);              set.add(randomInt);          }          StringBuffer sb = new StringBuffer();          for (Integer i : set)          {              sb.append("" + i);          }          return sb.toString();      }  

这里写图片描述

参考:http://blog.csdn.net/lmj623565791/article/details/24252901

原创粉丝点击