注:以下笔记都是通过《Android群英传》这本书整理而来。
〇、自定义View前篇
1、我们不能机械地记忆所有绘图的API,而是要让这些API为你所用,结合现实中的绘图方法,甚至是Photoshop的技巧,才能设计出更好的自定义View。
2、一个用户觉得熟悉的控件才是好的控件,切记勿追求华而不实。
3、View中比较重要的回调方法:
- onFinishInflate():从XML加载组件后回调。
- onSizeChanged():组件大小改变时。
- onMeasure():回调该方法来进行测量。
- onLayout():回调该方法来确定显示的位置。
- onTouchEvent():监听到触摸事件时回调。
一、对现有控件进行扩展
案例一:
part1:在构造中创建两支画笔,一支画背景一支画边框。
mPaint1 = new Paint();
mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);// 设置风格为实心
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
part2:在onDraw方法中绘制。
@Override
protected void onDraw(Canvas canvas) {
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint1);
//绘制内层矩形
canvas.drawRect(10,10,getMeasuredWidth()-10,getMeasuredHeight()-10,mPaint2);
canvas.save();
//绘制文字前平移10像素
canvas.translate(10,10);
//在回调父方法前,实现自己的逻辑,对TextView来说就是在绘制文本内容前
super.onDraw(canvas);
canvas.restore();
}
案例二:
part1:
1、在onSizeChanged中初始化一些对象,是因为在该方法中组件的大小才真正改变,才能拿到组件的长宽被这些对象所用。
2、利用Paint对象的Shader渲染器,通过设置一个不断变化的LinearGradient,并使用带该属性(渲染器)的Paint来绘制要显示的文字。
3、最关键的就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给Paint设置原生TextView没有的LinearGradient属性。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if(mViewWidth == 0){
mViewWidth = getMeasuredWidth();
if(mViewWidth>0){
mPaint = getPaint(); // 注意这个地方不要写成new Paint()
//参数一为渐变起初点坐标x位置,参数二为y轴位置,参数三和四分辨对应渐变终点,
//参数五是参与渐变效果的颜色集合
//参数六是定义每个颜色处于的渐变相对位置,这个参数可以为null,如果为null表示所有的颜色按顺序均匀的分布
//最后参数为平铺方式
mLinearGradient = new LinearGradient(
0,0,mViewWidth,0,new int[]{
Color.BLUE,0xffffffff,Color.BLUE
},null, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}
part2:通过矩阵的方式来不断平移渐变效果,在绘制文字时,产生动态的闪动效果。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mGradientMatrix!=null){
mTranslate += mViewWidth/10;
if(mTranslate > 2 * mViewWidth){
mTranslate = -mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate,0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(50);
}
}
二、创建复合控件
书上的案例太简单,代码有点多这里就不贴了。
简单总结一下就是分为四个步骤:
1、自定义属性
1、在res资源目录下values目录下创建attrs.xml属性定义文件。
<resource>
<declare-styleable name="一般这里写控件的名字">
<attr name="属性名" format="属性类型,可以用"|"分隔不同的属性"/>
</declare-styleable>
</resouce>
2、在构造中用代码获取xml布局中自定义的那些属性。
TypeArray ta = context.obtainStyledAttributes(attr,R.styleable.控件名);
属性类型 属性值 = ta.get属性类型(R.styleable.属性名);
ta.recycler();//调用recycler完成资源回收
2、组合控件
1、为创建的组件元素赋值,值就来源于我们在引用的xml文件中给对应属性的赋值。
Button btn1 = new Button(context);
btn1.setTextColor(属性值);
2、为组件元素设置响应的布局元素。
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
params .addRule(ALIGN_PARENT_RIGHT,TRUE);
3、添加到ViewGroup。
addView(btn1,params);
3、定义接口。(略过)
4、暴露接口给调用者。(略过)
三、重写View来实现全新的控件
案例三:
part1:在构造方法中初始化对象。
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int screenWidth = wm.getDefaultDisplay().getWidth();
//圆心
mCircleXY = screenWidth/2;
//圆的半径
mRadius = (float) (screenWidth*0.5/2);
//指定圆弧的外轮廓矩形区域
mArcRectF = new RectF((float)(screenWidth*0.1),(float)(screenWidth*0.1),(float)(screenWidth*0.9),(float)(screenWidth*0.9));
mCirclePaint = new Paint();
mCirclePaint.setColor(Color.BLUE);
mArcPaint = new Paint();
mArcPaint.setStyle(Paint.Style.STROKE);
mArcPaint.setStrokeWidth(50);
mArcPaint.setColor(Color.GREEN);
mTextPaint = new Paint();
mTextPaint.setColor(Color.BLACK);
mShowText = "哈哈哈哈";
mShowTextSize = 40;
part2:分别绘制圆、圆弧、文字。
@Override
protected void onDraw(Canvas canvas) {
//绘制圆
canvas.drawCircle(mCircleXY,mCircleXY,mRadius,mCirclePaint);
//绘制弧线(
// 参数1:指定圆弧的外轮廓矩形区域,
// 参数2:圆弧起始角度,单位为度
// 参数3:圆弧扫过的角度,顺时针方向,单位为度,从右中间开始为零度
// 参数4:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形
// 参数5:绘制圆弧的画板属性,如颜色,是否填充等。)
canvas.drawArc(mArcRectF,270,270,false,mArcPaint);
//绘制文字
canvas.drawText(mShowText,0,mShowText.length(),mCircleXY,mCircleXY+(mShowTextSize/4),mTextPaint);
}
相关资料:
android中canvas.drawText参数的介绍以及绘制一个文本居中的案例
Android的DrawText详解
如何“任性”使用Android的drawText()
drawArc方法介绍
案例四:
part1:先在构造中初始化一些对象
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mWidth = wm.getDefaultDisplay().getWidth();
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
part2:在组件改变位置的时候给Paint增加一个LinearGradient渐变效果。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getWidth();
mRectHeight= getHeight();
mRectWidth = (int)(mWidth*0.6/mRectCount);
LinearGradient mLinearGradient = new LinearGradient(0,0,mRectWidth,mRectHeight,
Color.YELLOW,Color.BLUE, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
}
part3:在onDraw中循环绘制。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0;i<mRectCount;i++){
double random = Math.random();
//每个矩形的高
float currentHeight = (float) (mRectHeight*random);
canvas.drawRect((float)(mWidth*0.4/2+mRectWidth*i+offset),
currentHeight,(float)(mWidth*0.4/2+mRectWidth*(i+1)),
mRectHeight,mPaint);
}
}
今天敲了一遍书上的案例,有些地方还不是很消化,例如绘制文字那块还是略模糊,明天继续更自定义ViewGroup。
四、自定义ViewGroup
1、ViewGroup的目的就是为了对其子View进行管理。
2、ViewGroup通常要重写onMeasure()方法对子View进行测量,重写onLayout()确定子View的位置,重写onTouchEvent()增加响应事件。
案例5:
//TODO:待插入gif图
part1:在构造函数中初始化一些对象:
mScroller = new Scroller(context);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mScreenHeight = wm.getDefaultDisplay().getHeight();
part2:在onMeasure中给子View进行测量。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i=0;i<count;i++){
View childView = getChildAt(i);
//用遍历的方式给子View测量
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
part3:遍历设定每个子View需要摆放的位置,直接通过调用子View的layout方法,将具体的参数传入即可。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
//设置ViewGroup的高度
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
//这里让每个子View都显示完整的一屏
mlp.height = mScreenHeight*childCount;
setLayoutParams(mlp);
for (int i = 0;i<childCount;i++){
View child = getChildAt(i);
if(child.getVisibility()!=GONE){
//修改每个子View的top和bottom两个属性,让他们可以依次排列下来
child.layout(l,i*mScreenHeight,r,(i+1)*mScreenHeight);
}
}
}
part4:在onTouchEvent中实现滚动的逻辑和"粘性"的逻辑。
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = y;
//记录触摸起点
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y;
if(getScrollY()<0){
dy = 0;
}
if(getScrollY()>getHeight()-mScreenHeight){
dy = 0;
}
scrollBy(0,dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
//记录触摸终点
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if(dScrollY>0){
if(dScrollY<mScreenHeight/3){
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}else{
mScroller.startScroll(0,getScrollY(),0,mScreenHeight-dScrollY);
}
}else{
if(-dScrollY<mScreenHeight/3){
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}else{
mScroller.startScroll(0,getScrollY(),0,-mScreenHeight-dScrollY);
}
}
break;
}
postInvalidate();
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0,mScroller.getCurrY());
postInvalidate();
}
}
网友评论
},为什么要mCircleXY+mShowTextSize/4, 这个地方不太明白。谢谢你的分享