版權(quán)聲明:本文為博主原創(chuàng)文章赖欣,未經(jīng)博主允許不得轉(zhuǎn)載。
系列教程:Android開發(fā)之從零開始系列
源碼:github.com/AnliaLee/FallingView毁嗦,歡迎star大家要是看到有錯(cuò)誤的地方或者有啥好的建議卵蛉,歡迎留言評(píng)論
前言:轉(zhuǎn)眼已是十一月下旬了瓜挽,天氣慢慢轉(zhuǎn)冷盹廷,不知道北方是不是已經(jīng)開始下雪了呢?本期教程我們就順應(yīng)季節(jié)主題久橙,一起來實(shí)現(xiàn) 雪花飄落的效果吧俄占。本篇效果思路參考自國外大神的Android實(shí)現(xiàn)雪花飛舞效果,并在此基礎(chǔ)上實(shí)現(xiàn)進(jìn)一步的封裝和功能擴(kuò)展
本篇只著重于思路和實(shí)現(xiàn)步驟淆衷,里面用到的一些知識(shí)原理不會(huì)非常細(xì)地拿來講缸榄,如果有不清楚的api或方法可以在網(wǎng)上搜下相應(yīng)的資料,肯定有大神講得非常清楚的祝拯,我這就不獻(xiàn)丑了甚带。本著認(rèn)真負(fù)責(zé)的精神我會(huì)把相關(guān)知識(shí)的博文鏈接也貼出來(其實(shí)就是懶不想寫那么多哈哈),大家可以自行傳送佳头。為了照顧第一次閱讀系列博客的小伙伴鹰贵,本篇會(huì)出現(xiàn)一些在之前系列博客就講過的內(nèi)容,看過的童鞋自行跳過該段即可
國際慣例畜晰,先上效果圖
目錄
- 繪制一個(gè)循環(huán)下落的“雪球”
- 封裝下落物體對(duì)象
- 擴(kuò)展一:增加導(dǎo)入Drawable資源的構(gòu)造方法和設(shè)置物體大小的接口
- 擴(kuò)展一:擴(kuò)展二:實(shí)現(xiàn)雪花“大小不一”砾莱、“快慢有別”的效果
- 擴(kuò)展三:引入“風(fēng)”的概念
繪制一個(gè)循環(huán)下落的“雪球”
我們先從最簡單的部分做起瑞筐,自定義View中實(shí)現(xiàn)循環(huán)動(dòng)畫的方法有很多凄鼻,最簡單直接的當(dāng)然是用Animation類去實(shí)現(xiàn),但考慮到無論是雪花聚假、雪球亦或是雨滴什么的块蚌,每個(gè)獨(dú)立的個(gè)體都有自己的起點(diǎn)、速度和方向等等膘格,其下落的過程會(huì)出現(xiàn)很多隨機(jī)的因素峭范,實(shí)現(xiàn)這種非規(guī)律的動(dòng)畫Animation類就不怎么適用了,因此我們這次要利用線程通信實(shí)現(xiàn)一個(gè)簡單的定時(shí)器瘪贱,達(dá)到周期性繪制View的效果纱控。這里我們簡單繪制一個(gè)“雪球”(其實(shí)就是個(gè)白色背景的圓形哈哈)來看看定時(shí)器的效果辆毡,新建一個(gè)FallingView
public class FallingView extends View {
private Context mContext;
private AttributeSet mAttrs;
private int viewWidth;
private int viewHeight;
private static final int defaultWidth = 600;//默認(rèn)寬度
private static final int defaultHeight = 1000;//默認(rèn)高度
private static final int intervalTime = 5;//重繪間隔時(shí)間
private Paint testPaint;
private int snowY;
public FallingView(Context context) {
super(context);
mContext = context;
init();
}
public FallingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
mAttrs = attrs;
init();
}
private void init(){
testPaint = new Paint();
testPaint.setColor(Color.WHITE);
testPaint.setStyle(Paint.Style.FILL);
snowY = 0;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = measureSize(defaultHeight, heightMeasureSpec);
int width = measureSize(defaultWidth, widthMeasureSpec);
setMeasuredDimension(width, height);
viewWidth = width;
viewHeight = height;
}
private int measureSize(int defaultSize,int measureSpec) {
int result = defaultSize;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(100,snowY,25,testPaint);
getHandler().postDelayed(runnable, intervalTime);//間隔一段時(shí)間再進(jìn)行重繪
}
// 重繪線程
private Runnable runnable = new Runnable() {
@Override
public void run() {
snowY += 15;
if(snowY>viewHeight){//超出屏幕則重置雪球位置
snowY = 0;
}
invalidate();
}
};
}
效果如圖
在上述代碼中View基本的框架我們已經(jīng)搭好了,思路其實(shí)很簡單甜害,我們需要做僅僅是在每次重繪之前更新做下落運(yùn)動(dòng)的物體的位置即可
封裝下落物體對(duì)象
相關(guān)博文鏈接
Android開發(fā)中無處不在的設(shè)計(jì)模式——Builder模式
[Android] 獲取View的寬度和高度
要實(shí)現(xiàn)大雪紛飛的效果舶掖,很明顯只有一個(gè)雪球是不夠的,而且雪也不能只有雪球一個(gè)形狀尔店,我們希望可以自定義雪的樣式眨攘,甚至不局限于下雪,還可以下雨嚣州、下金幣等等鲫售,因此我們要對(duì)下落的物體進(jìn)行封裝。為了以后物體類對(duì)外方法代碼的可讀性该肴,這里我們采用Builder設(shè)計(jì)模式來構(gòu)建物體對(duì)象類情竹,新建FallObject
public class FallObject {
private int initX;
private int initY;
private Random random;
private int parentWidth;//父容器寬度
private int parentHeight;//父容器高度
private float objectWidth;//下落物體寬度
private float objectHeight;//下落物體高度
public int initSpeed;//初始下降速度
public float presentX;//當(dāng)前位置X坐標(biāo)
public float presentY;//當(dāng)前位置Y坐標(biāo)
public float presentSpeed;//當(dāng)前下降速度
private Bitmap bitmap;
public Builder builder;
private static final int defaultSpeed = 10;//默認(rèn)下降速度
public FallObject(Builder builder, int parentWidth, int parentHeight){
random = new Random();
this.parentWidth = parentWidth;
this.parentHeight = parentHeight;
initX = random.nextInt(parentWidth);//隨機(jī)物體的X坐標(biāo)
initY = random.nextInt(parentHeight)- parentHeight;//隨機(jī)物體的Y坐標(biāo),并讓物體一開始從屏幕頂部下落
presentX = initX;
presentY = initY;
initSpeed = builder.initSpeed;
presentSpeed = initSpeed;
bitmap = builder.bitmap;
objectWidth = bitmap.getWidth();
objectHeight = bitmap.getHeight();
}
private FallObject(Builder builder) {
this.builder = builder;
initSpeed = builder.initSpeed;
bitmap = builder.bitmap;
}
public static final class Builder {
private int initSpeed;
private Bitmap bitmap;
public Builder(Bitmap bitmap) {
this.initSpeed = defaultSpeed;
this.bitmap = bitmap;
}
/**
* 設(shè)置物體的初始下落速度
* @param speed
* @return
*/
public Builder setSpeed(int speed) {
this.initSpeed = speed;
return this;
}
public FallObject build() {
return new FallObject(this);
}
}
/**
* 繪制物體對(duì)象
* @param canvas
*/
public void drawObject(Canvas canvas){
moveObject();
canvas.drawBitmap(bitmap,presentX,presentY,null);
}
/**
* 移動(dòng)物體對(duì)象
*/
private void moveObject(){
moveY();
if(presentY>parentHeight){
reset();
}
}
/**
* Y軸上的移動(dòng)邏輯
*/
private void moveY(){
presentY += presentSpeed;
}
/**
* 重置object位置
*/
private void reset(){
presentY = -objectHeight;
presentSpeed = initSpeed;
}
}
FallingView中相應(yīng)地設(shè)置添加物體的方法
public class FallingView extends View {
//省略部分代碼...
private List<FallObject> fallObjects;
private void init(){
fallObjects = new ArrayList<>();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(fallObjects.size()>0){
for (int i=0;i<fallObjects.size();i++) {
//然后進(jìn)行繪制
fallObjects.get(i).drawObject(canvas);
}
// 隔一段時(shí)間重繪一次, 動(dòng)畫效果
getHandler().postDelayed(runnable, intervalTime);
}
}
// 重繪線程
private Runnable runnable = new Runnable() {
@Override
public void run() {
invalidate();
}
};
/**
* 向View添加下落物體對(duì)象
* @param fallObject 下落物體對(duì)象
* @param num
*/
public void addFallObject(final FallObject fallObject, final int num) {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
for (int i = 0; i < num; i++) {
FallObject newFallObject = new FallObject(fallObject.builder,viewWidth,viewHeight);
fallObjects.add(newFallObject);
}
invalidate();
return true;
}
});
}
}
在Activity中向FallingView添加一些物體看看效果
//繪制雪球bitmap
snowPaint = new Paint();
snowPaint.setColor(Color.WHITE);
snowPaint.setStyle(Paint.Style.FILL);
bitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
bitmapCanvas.drawCircle(25,25,25,snowPaint);
//初始化一個(gè)雪球樣式的fallObject
FallObject.Builder builder = new FallObject.Builder(bitmap);
FallObject fallObject = builder
.setSpeed(10)
.build();
fallingView = (FallingView) findViewById(R.id.fallingView);
fallingView.addFallObject(fallObject,50);//添加50個(gè)雪球?qū)ο?
效果如圖
到這里我們完成了一個(gè)最基礎(chǔ)的下落物體類匀哄,下面開始擴(kuò)展功能和效果
擴(kuò)展一:增加導(dǎo)入Drawable資源的構(gòu)造方法和設(shè)置物體大小的接口
我們之前的FallObject類中Builder只支持bitmap的導(dǎo)入鲤妥,很多時(shí)候我們的圖片樣式都是從drawable資源文件夾中獲取的,每次都要將drawable轉(zhuǎn)成bitmap是件很麻煩的事拱雏,因此我們要在FallObject類中封裝drawable資源導(dǎo)入的構(gòu)造方法棉安,修改FallObject
public static final class Builder {
//省略部分代碼...
public Builder(Bitmap bitmap) {
this.initSpeed = defaultSpeed;
this.bitmap = bitmap;
}
public Builder(Drawable drawable) {
this.initSpeed = defaultSpeed;
this.bitmap = drawableToBitmap(drawable);
}
}
/**
* drawable圖片資源轉(zhuǎn)bitmap
* @param drawable
* @return
*/
public static Bitmap drawableToBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(
drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
: Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.draw(canvas);
return bitmap;
}
有了drawable資源導(dǎo)入的構(gòu)造方法,肯定需要配套改變FallObject圖片樣式大小的接口铸抑,依然是在FallObject的Builder中擴(kuò)展相應(yīng)的接口
public static final class Builder {
//省略部分代碼...
public Builder setSize(int w, int h){
this.bitmap = changeBitmapSize(this.bitmap,w,h);
return this;
}
}
/**
* 改變bitmap的大小
* @param bitmap 目標(biāo)bitmap
* @param newW 目標(biāo)寬度
* @param newH 目標(biāo)高度
* @return
*/
public static Bitmap changeBitmapSize(Bitmap bitmap, int newW, int newH) {
int oldW = bitmap.getWidth();
int oldH = bitmap.getHeight();
// 計(jì)算縮放比例
float scaleWidth = ((float) newW) / oldW;
float scaleHeight = ((float) newH) / oldH;
// 取得想要縮放的matrix參數(shù)
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
// 得到新的圖片
bitmap = Bitmap.createBitmap(bitmap, 0, 0, oldW, oldH, matrix, true);
return bitmap;
}
在Activity中初始化下落物體樣式時(shí)我們就可以導(dǎo)入drawable資源和設(shè)置物體大小了(圖片資源我是在阿里圖標(biāo)庫下載的)
FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
.setSpeed(10)
.setSize(50,50)
.build();
來看下效果
擴(kuò)展二:實(shí)現(xiàn)雪花“大小不一”贡耽、“快慢有別”的效果
之前我們通過導(dǎo)入drawable資源的方法讓屏幕“下起了雪花”,但雪花個(gè)個(gè)都一樣大小鹊汛,下落速度也都完全一致蒲赂,這顯得十分的單調(diào),看起來一點(diǎn)也不像現(xiàn)實(shí)中的下雪場景刁憋。因此我們需要利用隨機(jī)數(shù)實(shí)現(xiàn)雪花“大小不一”滥嘴、“快慢有別”的效果,修改FallObject
public class FallObject {
//省略部分代碼...
private boolean isSpeedRandom;//物體初始下降速度比例是否隨機(jī)
private boolean isSizeRandom;//物體初始大小比例是否隨機(jī)
public FallObject(Builder builder, int parentWidth, int parentHeight){
//省略部分代碼...
this.builder = builder;
isSpeedRandom = builder.isSpeedRandom;
isSizeRandom = builder.isSizeRandom;
initSpeed = builder.initSpeed;
randomSpeed();
randomSize();
}
private FallObject(Builder builder) {
//省略部分代碼...
isSpeedRandom = builder.isSpeedRandom;
isSizeRandom = builder.isSizeRandom;
}
public static final class Builder {
//省略部分代碼...
private boolean isSpeedRandom;
private boolean isSizeRandom;
public Builder(Bitmap bitmap) {
//省略部分代碼...
this.isSpeedRandom = false;
this.isSizeRandom = false;
}
public Builder(Drawable drawable) {
//省略部分代碼...
this.isSpeedRandom = false;
this.isSizeRandom = false;
}
/**
* 設(shè)置物體的初始下落速度
* @param speed
* @return
*/
public Builder setSpeed(int speed) {
this.initSpeed = speed;
return this;
}
/**
* 設(shè)置物體的初始下落速度
* @param speed
* @param isRandomSpeed 物體初始下降速度比例是否隨機(jī)
* @return
*/
public Builder setSpeed(int speed,boolean isRandomSpeed) {
this.initSpeed = speed;
this.isSpeedRandom = isRandomSpeed;
return this;
}
/**
* 設(shè)置物體大小
* @param w
* @param h
* @return
*/
public Builder setSize(int w, int h){
this.bitmap = changeBitmapSize(this.bitmap,w,h);
return this;
}
/**
* 設(shè)置物體大小
* @param w
* @param h
* @param isRandomSize 物體初始大小比例是否隨機(jī)
* @return
*/
public Builder setSize(int w, int h, boolean isRandomSize){
this.bitmap = changeBitmapSize(this.bitmap,w,h);
this.isSizeRandom = isRandomSize;
return this;
}
}
/**
* 重置object位置
*/
private void reset(){
presentY = -objectHeight;
randomSpeed();//記得重置時(shí)速度也一起重置至耻,這樣效果會(huì)好很多
}
/**
* 隨機(jī)物體初始下落速度
*/
private void randomSpeed(){
if(isSpeedRandom){
presentSpeed = (float)((random.nextInt(3)+1)*0.1+1)* initSpeed;//這些隨機(jī)數(shù)大家可以按自己的需要進(jìn)行調(diào)整
}else {
presentSpeed = initSpeed;
}
}
/**
* 隨機(jī)物體初始大小比例
*/
private void randomSize(){
if(isSizeRandom){
float r = (random.nextInt(10)+1)*0.1f;
float rW = r * builder.bitmap.getWidth();
float rH = r * builder.bitmap.getHeight();
bitmap = changeBitmapSize(builder.bitmap,(int)rW,(int)rH);
}else {
bitmap = builder.bitmap;
}
objectWidth = bitmap.getWidth();
objectHeight = bitmap.getHeight();
}
}
在Activity中設(shè)置相應(yīng)參數(shù)即可
FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
.setSpeed(10,true)
.setSize(50,50,true)
.build();
效果如圖若皱,是不是看起來感覺好多了?乛?乛?
擴(kuò)展三:引入“風(fēng)”的概念
“風(fēng)”其實(shí)是一種比喻,實(shí)際上要做的是讓雪花除了做下落運(yùn)動(dòng)外尘颓,還會(huì)橫向移動(dòng)走触,也就是說我們要模擬出雪花在風(fēng)中亂舞的效果。為了讓雪花在X軸上的位移不顯得鬼畜(大家可以直接隨機(jī)增減x坐標(biāo)值就知道為什么是鬼畜了哈哈)疤苹,我們采用正弦函數(shù)來獲取X軸上的位移距離互广,如圖所示
正弦函數(shù)曲線見下圖
我們選取-π到π這段曲線,可以看出角的弧度在為π/2時(shí)正弦值最大(-π/2時(shí)最小)惫皱,因此我們?cè)谟?jì)算角度時(shí)還需要考慮其極限值像樊。同時(shí),因?yàn)槲覀兲砑恿藱M向的移動(dòng)旅敷,所以判斷邊界時(shí)要記得判定最左和最右的邊界凶硅,修改FallObject
public class FallObject {
//省略部分代碼...
public int initSpeed;//初始下降速度
public int initWindLevel;//初始風(fēng)力等級(jí)
private float angle;//物體下落角度
private boolean isWindRandom;//物體初始風(fēng)向和風(fēng)力大小比例是否隨機(jī)
private boolean isWindChange;//物體下落過程中風(fēng)向和風(fēng)力是否產(chǎn)生隨機(jī)變化
private static final int defaultWindLevel = 0;//默認(rèn)風(fēng)力等級(jí)
private static final int defaultWindSpeed = 10;//默認(rèn)單位風(fēng)速
private static final float HALF_PI = (float) Math.PI / 2;//π/2
public FallObject(Builder builder, int parentWidth, int parentHeight){
//省略部分代碼...
isWindRandom = builder.isWindRandom;
isWindChange = builder.isWindChange;
initSpeed = builder.initSpeed;
randomSpeed();
randomSize();
randomWind();
}
private FallObject(Builder builder) {
//省略部分代碼...
isWindRandom = builder.isWindRandom;
isWindChange = builder.isWindChange;
}
public static final class Builder {
//省略部分代碼...
private boolean isWindRandom;
private boolean isWindChange;
public Builder(Bitmap bitmap) {
//省略部分代碼...
this.isWindRandom = false;
this.isWindChange = false;
}
public Builder(Drawable drawable) {
//省略部分代碼...
this.isWindRandom = false;
this.isWindChange = false;
}
/**
* 設(shè)置風(fēng)力等級(jí)、方向以及隨機(jī)因素
* @param level 風(fēng)力等級(jí)(絕對(duì)值為 5 時(shí)效果會(huì)比較好)扫皱,為正時(shí)風(fēng)從左向右吹(物體向X軸正方向偏移)足绅,為負(fù)時(shí)則相反
* @param isWindRandom 物體初始風(fēng)向和風(fēng)力大小比例是否隨機(jī)
* @param isWindChange 在物體下落過程中風(fēng)的風(fēng)向和風(fēng)力是否會(huì)產(chǎn)生隨機(jī)變化
* @return
*/
public Builder setWind(int level,boolean isWindRandom,boolean isWindChange){
this.initWindLevel = level;
this.isWindRandom = isWindRandom;
this.isWindChange = isWindChange;
return this;
}
}
/**
* 移動(dòng)物體對(duì)象
*/
private void moveObject(){
moveX();
moveY();
if(presentY>parentHeight || presentX<-bitmap.getWidth() || presentX>parentWidth+bitmap.getWidth()){
reset();
}
}
/**
* X軸上的移動(dòng)邏輯
*/
private void moveX(){
presentX += defaultWindSpeed * Math.sin(angle);
if(isWindChange){
angle += (float) (random.nextBoolean()?-1:1) * Math.random() * 0.0025;
}
}
/**
* 重置object位置
*/
private void reset(){
presentY = -objectHeight;
randomSpeed();//記得重置時(shí)速度也一起重置,這樣效果會(huì)好很多
randomWind();//記得重置一下初始角度韩脑,不然雪花會(huì)越下越少(因?yàn)榻嵌壤奂訒?huì)讓雪花越下越偏)
}
/**
* 隨機(jī)風(fēng)的風(fēng)向和風(fēng)力大小比例氢妈,即隨機(jī)物體初始下落角度
*/
private void randomWind(){
if(isWindRandom){
angle = (float) ((random.nextBoolean()?-1:1) * Math.random() * initWindLevel /50);
}else {
angle = (float) initWindLevel /50;
}
//限制angle的最大最小值
if(angle>HALF_PI){
angle = HALF_PI;
}else if(angle<-HALF_PI){
angle = -HALF_PI;
}
}
}
在Activity中調(diào)用新增加的接口
FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
.setSpeed(7,true)
.setSize(50,50,true)
.setWind(5,true,true)
.build();
效果如圖
至此本篇教程到此結(jié)束,如果大家看了感覺還不錯(cuò)麻煩點(diǎn)個(gè)贊段多,你們的支持是我最大的動(dòng)力~