原创文章,转载注明
先看一下效果图吧
这里说一下关键的思路,所有的细节都说到太麻烦了。
1、沙漏的绘制
上下两边的绘制,用二阶贝塞尔曲线,先确定左端点,即可获得对称的右端点,上边中间控制点为(屏幕x/2,y>左右端点y),下边中间控制点为(屏幕X/2,y<左右端点)
左右两边的绘制,用二阶贝塞尔曲线,上下两边的端点已知,另一端点为屏幕中间点加或减一定的阈值,这样才可以留出沙子下落的通道。控制点 为两端点间的高度再比较靠上的位置。
2、下沙堆的绘制
下沙堆比较容易画出,一样用二阶贝塞尔曲线,端点分别为沙漏下端两端点,控制点为(屏幕x/2,y>沙漏底边控制点y),随着时间的减少,只需要将控制点的y坐标逐渐加大到屏幕y/2即可。
3、上沙堆的绘制
上沙堆的绘制比较难,原因在于android只提供了贝尔塞尔曲线的绘制,没有提供获取贝塞尔曲线任意点的坐标,比如我想在沙漏上部分1/2处找到左右两边上对应的点坐标就没办法做到了。由于贝尔塞尔曲线是用插值方程作为轨迹方程的。因此我们可以通过该方程获得点坐标。而对于插值,它的范围为0-1,这里将插值设为1/2即可获得中点坐标。到此为止,端点以及获得了,但是此处不能用贝塞尔曲线了,因为控制点无法保证图像能填充慢整个沙堆到中心点。因此这里需要用三角形,也就是说我们之前获得的两个端点为三角形底边的端点。另外一点为屏幕中点。为了弥补沙堆边缘不能填充慢整个沙漏的问题,插值不能设置的过小,也就是说沙堆不能太高。最后,上面沙堆的顶部弧形,这个就可以用二阶贝塞尔曲线了,端点已知,控制点为(屏幕x/2.y>端点y)。
4、落下的沙粒动画
这个就不细说了,想怎么实现怎么实现
5、时间和沙堆变化的关联
我们知道下沙堆变高是通过改变控制点,我们只需要把时间和控制点的最高,最低y坐标的差值建立关系即可。
对于上沙堆,它的变化是通过左右两端点的下滑实现的,然而左右两端点的变化在二阶贝塞尔曲线的轨迹方程上,然而,贝塞尔曲线的轨迹方程为插值方程,那么我们只需要改变相应的插值即可。因此,我们只需要把时间和沙漏上部左右两边的贝塞尔方程的插值建立关系即可。
最后上代码
贝尔塞尔曲线轨迹方程
package com.xf.ztime.base.util;
/**
*
* 提供平面坐标的一系列计算
*
* @author 吴林峰
*
*/
public class Point {
public int x;
public int y;
/**
*
* 通过直线插值方程计算任意点
*
* @param t
* @param p0
* @param p1
* @return
*/
public static Point interpolationLine(float t,Point p0,Point p1){
if(t<0 || t>1)
return null;
Point point=new Point();
point.setX((int) ((1-t)*p0.x+t*p1.x));
point.setY((int) ((1-t)*p0.y+t*p1.y));
return point;
}
/**
*
* 通过二阶贝塞尔曲线方程计算任意点
*
* @param t
* @param p0
* @param p1
* @param p2
* @return
*/
public static Point bezier(float t,Point p0, Point p1, Point p2){
if(t<0 || t>1)
return null;
Point point=new Point();
point.setX((int) ((1-t)*(1-t)*p0.x+2*t*(1-t)*p1.x+t*t*p2.x));
point.setY((int) ((1-t)*(1-t)*p0.y+2*t*(1-t)*p1.y+t*t*p2.y));
return point;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
package com.xf.ztime.countDown.view;
import com.example.ztime.R;
import com.xf.ztime.base.util.Point;
import com.xf.ztime.base.util.timeFactory.SystemTimeFactory;
import com.xf.ztime.base.util.timeFactory.TimeFactory;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
/**
*
* 沙漏动画的实现,可以自己指定大小
*
* @author 吴林峰
*
*/
public class HourglassView extends View{
private static final String TAG="HourglassView";
public static final String STATE_RUN="run";
public static final String STATE_START="start";
public static final String STATE_PAUSE="pause";
public static final String STATE_END="end";
private String state=STATE_RUN;
private Context context;
private Thread refreshThread;//刷新时间线程
private float refresh_time = 1000;//刷新的时间
private int count=0;
private int maxSendCount=20; //移动的沙子最大数量
private int currentSend=0; //当前移动的沙子数量
private int sendMaxHeight; //移动沙子的最大高度
private boolean isSizeParamgramable; //是否可以通过编程设定宽高
private int marginLR; //向左右边缘点横向margin,左上角点的x
private int marginR; //右边缘点的x
private int topOffset; //最高点,顶点y的偏移量
private int marginLRY; //顶部左右边缘点纵向margin,左上角点,右上角点的y
private int topY; //最高点,顶点的y
private int bottom; //底部左右边缘纵向margin,左下角点,右下角点的y
private int centerX; //中心点x
private int centerY; //中心点y
private int controlLX; //左控制点X
private int controlTY; //上控制点y
private int controlRX; //右控制点x
private int controlBY; //下控制点y
private Point pointTL=new Point(); //左上角端点
private Point pointTR=new Point(); //右上角端点
private Point pointControlLT=new Point(); //左上控制点
private Point pointControlRT=new Point(); //右上控制点
private Point pointCenter=new Point(); //中心点
private Point pointCenterLOffset=new Point(); //中心左偏移点
private Point pointCenterROffset=new Point(); //中心右偏移点
private float mWidth = 1000;//当宽为wrap_content时,默认的宽度
private float mHeight = 1200;//当高为wrap_content时,默认的高度
private float scaleT; //上部沙堆和时间的比例
private float scaleControlY; //底部沙堆控制点y轴坐标与时间的比例
private String time;
private TimeFactory timeFactory;
public HourglassView(Context context) {
this(context, null, 0);
}
public HourglassView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
this.context=context;
}
public HourglassView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.clock);
refresh_time = ta.getFloat(R.styleable.clock_refresh_time, 1000);
ta.recycle();
}
private void init() {
refreshThread = new Thread();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//添加了适应wrap_content的界面计算
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int) mWidth, (int) mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int) mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, (int) mHeight);
}
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
initParam();
Log.d(TAG, "state:"+state);
if(state.equals(STATE_START)){
initScale();
state=STATE_RUN;
}
float t=calculateT();
float y=calculateBottomSendControlHeight();
drawTopSend(canvas,t);
drawBottomSend(canvas,y);
drawDropSend(canvas);
drawTime(canvas);
drawTop(canvas);
drawSide(canvas);
drawBottom(canvas);
}
private void initParam(){
if(timeFactory!=null)
time=timeFactory.createTimeGetter().getTime();
if(state.equals(STATE_END))
time="0:0:0";
if(isSizeParamgramable){
setMeasuredDimension((int) mWidth, (int) mHeight);
return;
}
this.mWidth = Math.min(getWidth(), getHeight());
this.mHeight = Math.max(getWidth(), getHeight());
//向左右边缘点横向margin,左上角点的x
marginLR=(int) (this.mWidth/10);
//右边缘点的x
marginR=(int) mWidth-marginLR;
//最高点,顶点y的偏移量
topOffset=(int) (this.mHeight/50);
//底部左右边缘纵向margin,左下角点,右下角点的y
bottom=(int) (mHeight-topOffset);
//顶部左右边缘点纵向margin,左上角点,右上角点的y
marginLRY=topOffset*2;
//最高点,顶点的y
topY=marginLRY-30;
//中心点x
centerX=(int) (mWidth/2);
//中心点y
centerY=(int) (mHeight/2);
//左控制点X
controlLX=marginLR+10;
//上控制点y
controlTY=(int) (mHeight/4);
//右控制点x
controlRX=(int) (mWidth-marginLR-10);
//下控制点y
controlBY=(int) (mHeight*3/4);
//左上角端点
pointTL.setX(marginLR);
pointTL.setY(marginLRY);
//右上角端点
pointTR.setX(marginR);
pointTR.setY(marginLRY);
//左上控制点
pointControlLT.setX(controlLX);
pointControlLT.setY(controlTY);
//右上控制点
pointControlRT.setX(controlRX);
pointControlRT.setY(controlTY);
//中心点
pointCenter.setX(centerX);
pointCenter.setY(centerY);
//中心左偏移点
pointCenterLOffset.setX((int) (centerX-10));
pointCenterLOffset.setY(centerY);
//中心右偏移点
pointCenterROffset.setX(centerX+10);
pointCenterROffset.setY(centerY);
}
/**
*
* 画上部沙堆
*
* @param canvas
*/
private void drawTopSend(Canvas canvas,float t){
Log.d(TAG,"t:"+t);
if(t==1)
return;
//顶部沙堆左侧点
Point sendPointTL=Point.bezier(t, pointTL, pointControlLT, pointCenterLOffset);
//底部沙堆右侧点
Point sendPointTR=Point.bezier(t, pointTR, pointControlRT, pointCenterROffset);
//初始化上部沙堆
//为了解决填充问题,上部分是贝塞尔曲线。下部分贝塞尔曲线可能不会经过控制点,所以下部分为封闭三角形,因此t不能太小
Path sendTopPath1=new Path();
Path sendTopPath2=new Path();
Paint sendTopPaint1=new Paint();
Paint sendTopPaint2=new Paint();
sendTopPaint1.setColor(context.getResources().getColor(R.color.send));
sendTopPaint1.setStyle(Paint.Style.FILL);
sendTopPaint2.setColor(context.getResources().getColor(R.color.send));
sendTopPaint2.setStyle(Paint.Style.FILL);
sendTopPath1.moveTo(sendPointTL.x, sendPointTL.y);
sendTopPath1.quadTo(centerX, mHeight*2/10, sendPointTR.x,sendPointTR.y);
//sendTopPath1.moveTo((centerX-marginLR)/2+16, mHeight*3/10);
//sendTopPath1.quadTo(centerX, mHeight*2/10, centerX+(centerX+marginLR)/2-16, mHeight*3/10);
canvas.drawPath(sendTopPath1, sendTopPaint1);
sendTopPath2.moveTo(sendPointTL.x, sendPointTL.y);
//sendTopPath2.moveTo((centerX-marginLR)/2+16, mHeight*3/10);
sendTopPath2.lineTo(centerX, centerY);
sendTopPath2.lineTo(sendPointTR.x,sendPointTR.y);
//sendTopPath2.lineTo(centerX+(centerX+marginLR)/2-16, mHeight*3/10);
sendTopPath2.close();
canvas.drawPath(sendTopPath2, sendTopPaint2);
}
/**
*
* 画下部沙堆
*
* @param canvas
*/
private void drawBottomSend(Canvas canvas,float y){
Log.d(TAG,"y:"+y);
if(y==0)
y=mHeight/10;
Path sendPath=new Path();
Paint sendPaint=new Paint();
sendPaint.setColor(context.getResources().getColor(R.color.send));
sendPaint.setStyle(Paint.Style.FILL);
sendPath.moveTo(marginLR, bottom);
sendPath.quadTo(centerX, y, marginR, bottom);
canvas.drawPath(sendPath, sendPaint);
}
/**
*
* 画落下的沙子
*
* @param canvas
*/
private void drawDropSend(Canvas canvas){
if(state.equals(STATE_PAUSE) || state.equals(STATE_END))
return;
if(timeFactory!=null){
Paint pointPaint=new Paint();
pointPaint.setStrokeWidth(10f);
pointPaint.setColor(context.getResources().getColor(R.color.send));
pointPaint.setStyle(Paint.Style.FILL);
if(currentSend0){
if(sendMaxHeight