这里写了三个大的方面希望记录一下。当然,最大的收获是发现Aige的博客专栏,自定义控件其实很简单。在那里弄懂了很多,也同样看到了自己的距离。但这些可能并不是意识到就能够有变化的。我的观点观点和他的一样,自定义控件,首先要会画,要能画当然要掌握基本的方法。而往往,看到一大堆的方法,好似懂了一两个用法,就不愿去耐心尝试下其它的用法致手上的工具不足。而Aige讲解的控件的测量方面,也是获益匪浅,但其中的ViewGroup部分还留待去消,导化实践。
先从最简单的记起,事件分发。事件分发是从ViewGroup开始传递的。首先是从 ViewGroup#dispatchTouchEvent 方法开始的,也就是事件分发。如果其中 ViewGroup#onInterceptTouchEvent 方法返回true,就把事件在这里拦截,不再去寻找对应的子控件,而是直接在自己ViewGroup的 super#dispatchTouchEvent 中进行处理。onInterceptTouchEvent() 方法是ViewGroup独有的,它的作用是对事件进行拦截,默认返回false。如果ViewGroup#onInterceptTouchEvent 返回false,那么在接下来的代码实现中,就会去寻找对应的子控件,然后进入到该控件的View#dispatchTouchEvent。那么当找到子控件或者ViewGroup自己处理,最后都是到了View#dispatchTouchEVent 方法中。该方法首先调用onTouch()方法,如果有注册触摸事件监听的话。如果onTouch 方法返回true,那么事件分发就结束了。如果返回的不是true,那么就会执行onTouchEvent 方法。onTouchEvent 方法中对ACTION_DOWN、ACTION_UP,等事件进行处理,并在ACTION_UP中进行一段的逻辑判断后,执行 performClick() 方法,也就是执行我们注册的 onClick() 方法。
属性动画之前也认真的学过,结果前一两天要写时,却忘记得较干脆。之前在ToggleButton中也提到过rebound库可能和插值器这部分密切相关,这次重新学习时,发现正是其中的机制。先看基本的使用,主要是ObjectAnmator 类,例如:
ValueAnimator anim = ObjectAnimator.ofInt(mImageView, "translationY", mHeight);
anim.setDuration(5000)
.setInterpolator(new AccelerateDecelerateInterpolator());
anim.setRepeatCount(4);
anim.setRepeatMode(ValueAnimator.REVERSE);
anim.addListener(new Animator.AnimatorListener(){
@Override
public void onAnimationStart(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationEnd(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationCancel(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationRepeat(Animator animation) {
// TODO Auto-generated method stub
}
});
anim.start();
如果要对动画一起播放,顺序播放,可以使用AnimatorSet 类的相关方法。同样,也可以在xml中定义动画。在参考博文中有说明举例。
接下来看ValueAnimator的用法,属性动画的原理是对数值进行变化,然后用该变化的数值对控件的属性进行设置更新。ValueAnimator类中的有个注册监听方法:
public void addUpdateListener(AnimatorUpdateListener listener)
它的参数是个接口,在接口的回调方法中,
public static interface AnimatorUpdateListener {
/**
* <p>Notifies the occurrence of another frame of the animation.</p>
*
* @param animation The animation which was repeated.
*/
void onAnimationUpdate(ValueAnimator animation);
}
我们可以通过animation#getAnimatedValue() 获取到变化的对象(可能是整型值,可能是任何其它对象)。那么它的变化规律是在哪里如何控制的呢?它是在一个实现TypeEvaluator的接口的类中传递过去的。TypeEvaluator 类型估值器只有一个方法:
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
我们在这里的返回值,就是在监听接口用 getAnimatorValue 方法得到的值。参数fraction,范围在0到1,表示的动画的进度百分比;参数startValue和嗯对Value表示的传进来起止对象(不一定是数值对象),从哪里传进来呢,后面的例子中可以明显看到。那么在这里我们就可以做映射处理。那么还有个问题,就是fraction是以怎样的规律传进来呢?这里就涉及到另一个概念,就是插值器Interpolator的使用。系统有默认的实现和配置。这些类的共同点都是实现了 TimeInterpolator接口。可以在Eclispe中直接查看或查找文档。 TimeInterpolator 接口:
public interface TimeInterpolator {
float getInterpolation(float input);
}
只有一个接口方法,input是匀速的时间流逝比例。返回值就是我们的fraction值。在这里我们可以实现各种不同的数率变化,比如简单的匀速变化:
/**
* An interpolator where the rate of change is constant
*
*/
public class LinearInterpolator implements Interpolator {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
}
或者其它很复杂的模型。
参考博文中,给出了一个例子,这里我也自己实现了,贴下相关的代码:
BallPoint endBallPoint = new BallPoint(mWidth/2, mHeight-mRadius, Color.RED);
BallPoint startBallPoint = new BallPoint(mWidth/2, mRadius, Color.BLUE);
mCurrentBallPoint = startBallPoint;
ofObject = ValueAnimator.ofObject(new BallPointEvaluator(), startBallPoint, endBallPoint);
ofObject.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
BallPoint animatedValue = (BallPoint) animation.getAnimatedValue();
mCurrentBallPoint = animatedValue;
mBallPointPaint.setColor(mCurrentBallPoint.getColor());
log("F", "mcurrentBallPoint:" + mCurrentBallPoint.toString());
invalidate();
}
});
ofObject.setDuration(3000);
ofObject.setRepeatCount(3);
ofObject.setRepeatMode(ValueAnimator.RESTART);
// ofObject.setInterpolator(new BounceInterpolator());
ofObject.setInterpolator(new MyInterpolator());
ofObject.start();
private class BallPointEvaluator implements TypeEvaluator<BallPoint> {
@Override
public BallPoint evaluate(float fraction, BallPoint startValue,
BallPoint endValue) {
int x = (int) (startValue.getX() + fraction * (endValue.getX() - startValue.getX()));
int y = (int) (startValue.getY() + fraction * (endValue.getY() - startValue.getY()));
int oriColor = startValue.getColor();
int dstColor = endValue.getColor();
int or = Color.red(oriColor);
int og = Color.green(oriColor);
int ob = Color.blue(oriColor);
int dr = Color.red(dstColor);
int dg = Color.green(dstColor);
int db = Color.blue(dstColor);
int diffr = (int) (or + fraction * (dr - or));
int diffg = (int) (og + fraction * (dg - og));
int diffb = (int) (ob + fraction * (db - ob));
int diffColor = Color.rgb(diffr, diffg, diffb);
BallPoint ballPoint = new BallPoint(x, y, diffColor);
return ballPoint;
}
}
private class MyInterpolator implements TimeInterpolator {
@Override
public float getInterpolation(float input) {
float f = input * input;
return f;
}
}
代码都很简单,或者在上面都解释过了。
自定义控件,首先要画一个漂亮的控件。如果作画时以设计的角度去思考如何实现,可能会更简单一些。Paint,画笔。可以设置各种属性,比如 setColorFilter,对图片的颜色的处理,可以变化成各种风格,比如怀旧老照片,变暗,色彩增强等;比如 setXfermode,对两张图片进行各种混合处理,通过Alpha通道合成,它的各种模式都注释有计算公式。通过 setXfermode 可以实现多种效果;比如 setMaskFilter,可以设置过滤的效果,比如边缘的模糊处理;比如 setPathEffect,可以设置路径的连接方式;比如 setShader,可以设置画笔的遮罩模式,可以实现探照灯、圆形头像提取;等等等。Canvas,画布的容器,图形的承载者。可以缩放,平移,分层。
有些累,不写了。其中跟着Aige实现了大部分,这里贴两个图,一个是图标连接,另一个是模拟水杯的效果;这个用高阶贝塞尔曲线来完成的,和360的内存球的效果很类似。有空要练习,可以考虑实现360的内存球效果。
①
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG|Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mCircleWidth);
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mTextPaint.setTextSize(18);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mArcPaint.setStyle(Paint.Style.FILL);
mArcPaint.setColor(0x65ec6941);
mArcPaint.setStrokeWidth(mCircleWidth);
FontMetrics fontMetrics = mTextPaint.getFontMetrics();
// mTextAlignOffestY = (fontMetrics.descent - fontMetrics.ascent ) / 4; //这里怎么会是除以4呢?!,错误
mTextAlignOffestY = Math.abs(fontMetrics.ascent) / 2 - fontMetrics.descent /2 ; //正确
log("ascent:" + mTextPaint.getFontMetrics().ascent);
log("descent:" + mTextPaint.getFontMetrics().descent );
log("mTextAlignOffestY:" + mTextAlignOffestY);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.parseColor("#FF8C00"));
mPaint.setColor(Color.WHITE);
//中间的圆
canvas.drawCircle(mWidth/2, mHeight/2+mRadius, mRadius, mPaint);
canvas.drawText("AigeStudio", mWidth/2, mHeight/2+mRadius+mTextAlignOffestY, mTextPaint);
// canvas.drawLine(0, mHeight/2+mRadius, mWidth, mHeight/2+mRadius, mPaint);
// canvas.drawLine(0, mHeight/2+mRadius+mTextAlignOffestY, mWidth, mHeight/2+mRadius+mTextAlignOffestY, mPaint);
//画左上角的两个圆
canvas.save();
canvas.rotate(mDegrees, mWidth/2, mHeight/2+mRadius);
canvas.drawLine(mWidth/2, mHeight/2, mWidth/2, mHeight/2-mLineLength, mPaint);
canvas.drawCircle(mWidth/2, mHeight/2-mLineLength-mRadius, mRadius, mPaint);
canvas.drawText("loveever", mWidth/2, mHeight/2-mLineLength-mRadius+mTextAlignOffestY, mTextPaint);
canvas.drawLine(mWidth/2, mHeight/2-mLineLength-2*mRadius, mWidth/2, mHeight/2-mLineLength-2*mRadius-mLineLength, mPaint);
canvas.drawCircle(mWidth/2, mHeight/2-mLineLength-2*mRadius-mLineLength-mRadius, mRadius, mPaint);
canvas.drawText("mylife", mWidth/2, mHeight/2-mLineLength-2*mRadius-mLineLength-mRadius+mTextAlignOffestY, mTextPaint);
canvas.restore();
//画右上角的圆
canvas.save();
canvas.rotate(-mDegrees, mWidth/2, mHeight/2+mRadius);
canvas.drawLine(mWidth/2, mHeight/2, mWidth/2, mHeight/2-mLineLength, mPaint);
canvas.drawCircle(mWidth/2, mHeight/2-mLineLength-mRadius, mRadius, mPaint);
canvas.drawText("somethings", mWidth/2, mHeight/2-mLineLength-mRadius+mTextAlignOffestY, mTextPaint);
float rectOffestXY = mRadius + mRadius/2;
RectF oval = new RectF(mWidth/2-rectOffestXY, mHeight/2-mLineLength-mRadius-rectOffestXY, mWidth/2+rectOffestXY, mHeight/2-mLineLength-mRadius+rectOffestXY);
canvas.drawArc(oval, -150f, 120f, true, mArcPaint);
mArcPaint.setColor(Color.WHITE);
mArcPaint.setStyle(Paint.Style.STROKE);
canvas.drawArc(oval, -150f, 120f, false, mArcPaint);
float baseArc = 120 / 3;
float offestArc = 30;
float centerX = mWidth/2;
float centerY = mHeight/2-mLineLength-mRadius;
drawArcText(canvas, "Milk", rectOffestXY, offestArc, -1, centerX, centerY);
drawArcText(canvas, "FFFFF", rectOffestXY, offestArc+baseArc, -1, centerX, centerY);
drawArcText(canvas, "Yes", rectOffestXY, offestArc, 1, centerX, centerY);
drawArcText(canvas, "Love", rectOffestXY, offestArc+offestArc, 1, centerX, centerY);
canvas.restore();
//画左边的圆
canvas.save();
canvas.rotate(mDegrees+(2*mDegrees)+mDegrees*0.5f, mWidth/2, mHeight/2+mRadius);
canvas.drawLine(mWidth/2, mHeight/2-mMarginSpace, mWidth/2, mHeight/2-mLineLength-mMarginSpace, mPaint);
canvas.drawCircle(mWidth/2, mHeight/2-mLineLength-mRadiusSmall-mMarginSpace*2, mRadiusSmall, mPaint);
canvas.drawText("Apple", mWidth/2, mHeight/2-mLineLength-mRadiusSmall-mMarginSpace*2+mTextAlignOffestY, mTextPaint);
canvas.restore();
//画下边的圆
canvas.save();
canvas.rotate(6*mDegrees, mWidth/2, mHeight/2+mRadius);
canvas.drawLine(mWidth/2, mHeight/2-mMarginSpace, mWidth/2, mHeight/2-mLineLength-mMarginSpace, mPaint);
canvas.drawCircle(mWidth/2, mHeight/2-mLineLength-mRadiusSmall-mMarginSpace*2, mRadiusSmall, mPaint);
canvas.drawText("Pear", mWidth/2, mHeight/2-mLineLength-mRadiusSmall-mMarginSpace*2+mTextAlignOffestY, mTextPaint);
canvas.restore();
//画右边的圆
canvas.save();
canvas.rotate(-mDegrees-(2*mDegrees)-mDegrees*0.5f, mWidth/2, mHeight/2+mRadius);
canvas.drawLine(mWidth/2, mHeight/2-mMarginSpace, mWidth/2, mHeight/2-mLineLength-mMarginSpace, mPaint);
canvas.drawCircle(mWidth/2, mHeight/2-mLineLength-mRadiusSmall-mMarginSpace*2, mRadiusSmall, mPaint);
canvas.drawText("Orange", mWidth/2, mHeight/2-mLineLength-mRadiusSmall-mMarginSpace*2+mTextAlignOffestY, mTextPaint);
canvas.restore();
}
private void drawArcText(Canvas canvas, String string, float radius, float degress, int flag, float centerX, float centerY) {
log("String:" + string + ", out radius:" + radius + ", degress:" + degress + ", flag: " + flag + ", centerX:" + centerX + ", centerY" + centerY);
float disX = (float) (Math.cos(degress*Math.PI/180)*radius);
float disY = (float) (Math.sin(degress*Math.PI/180)*radius) + 10;
float x = (float) (disX * flag + centerX);
float y = (float) (-disY + centerY);
canvas.drawText(string, x, y, mTextPaint);
log("disX:" + disX + ", disY:" + disY);
}
②
@Override
protected void init() {
mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPathPaint.setColor(Color.GREEN);
mPathPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.LTGRAY);
mPath.reset();
mPath.moveTo(-mWidth /4, mCurrentY);
mPath.quadTo(mCurrentX, mCurrentY-mWaterHeightOffset, mWidth+mWidth/4, mCurrentY);
mPath.lineTo(mWidth+mWidth/4, mHeight);
mPath.lineTo(-mWidth/4, mHeight);
mPath.close();
mCurrentY = mInitY + mIncY++;
if (mCurrentY >= mHeight ) {
mIncY = (int) 1;
}
if (mCurrentX >= mWidth ) {
isRight = false;
} else if (mCurrentX <= 0 ) {
isRight = true;
}
mCurrentX = isRight ? mCurrentX+40 : mCurrentX-40;
canvas.drawPath(mPath, mPathPaint);
// log("F", "mCurrentX:" + mCurrentX + ", mCurrentY:" + mCurrentY);
invalidate();
}
其中, 该博文中对View的测量的流程,以及为什么要这样做,讲解的非常清楚。
参考博文:
2. Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
4. Android属性动画完全解析(上),初识属性动画的基本用法