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.testviews
namespace的别名。我们也可以选择其他的别名作为我们的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
- Android学习之创建自定义View(入门)
- android学习之(2)----自定义View
- Android 学习之自定义View
- Android学习之自定义View
- Android 自定义View-android学习之旅(十四)
- Android自定义View入门
- Android自定义View入门
- Android 自定义View入门
- Android自定义View入门
- Android自定义View入门
- Android--自定义View入门
- Android自定义view之(刻度尺view)
- Android学习笔记之自定义View(钢琴键盘部件)
- Android学习开发 之 自定义view
- Android学习之自定义view(一)
- Android学习之自定义view(二)
- Android学习之自定义view(三)
- Android 自定义View学习之文字绘制
- Android开源项目分类汇总
- 自定义的信号和槽实例
- CSS笔记
- typedef struct与struct的区别
- vue2.0安装和实例
- Android学习之创建自定义View(入门)
- Codeforces Round #392 (Div. 2) 758C Unfair Poll
- Postgresql递归查询
- mybatis接口方式编程
- Data structure-5 二叉搜索树 BST--Java语言实现
- 1018 单词接龙
- 【学生】优化(一)
- C#ListView详解(一)
- LA 4727 约瑟夫环 求最后n个数