標籤:gets 紅色 family bit 計算 虛線 ref boolean 固定高度
1,昨天看到了一個挺好的ui效果,是使用貝茲路徑實現的,就和大家來分享分享,還有,在寫部落格的時候我經常會把自己在做某種效果時的一些問題給寫出來,而不是像很多文章直接就給出瞭解決方法,這裡給大家解釋一下,這裡寫出我遇到的一些問題不是為了湊整片文章的字數,而是希望大家能從根源下知道它是怎麼解決的,而不是你直接百度搜尋這個問題解決的代碼,好了,說了這麼多,只是想告訴大家,我後面會在過程中提很多問題(邪惡臉,嘿嘿嘿),好吧,來看看今天的效果:
2,what is the fuck?,這就是你說的很好看的效果?各位看官別著急,這裡小弟也沒辦法,實在是找不到好的UI圖,就只能請各位將就一下了,好了言歸正傳,當我們看到這種效果的時候,我們已經有了一些思路,如下:
1,使用paint繪製正弦函數(調用Math.sin(x)的方法)2,使用逐幀動畫來實現3,使用貝塞爾三階來實現波浪效果
可能大家還有更多更好的方法,這上面幾點只是我能想到的幾點方法,我今天是使用的貝塞爾來實現的,不清楚貝塞爾使用的同學可以在我部落格分類的系列中找到這一欄的分類。
OK,我們先不要去管那些動畫,我們一步一步的來,那麼我們的視圖就只有兩部分了,一個是粉紅色帶水地區,一個是我們中間隨著動的icon圖片,那我們先來實現第一個粉紅色帶水的地方,我們最後要實現的效果如下:
ok,為了我們控制項的擴充性,我們這裡自訂一些屬性,這裡我們同學可以先不要理解這一塊(等全部理解之後再來看這一塊)
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="WaveView"> <!--中間小船的圖片--> <attr name="imageBitmap" format="reference"></attr> <!--水位是否要上升--> <attr name="rise" format="boolean"></attr> <!--水波紋向右移動的時候執行的時間--> <attr name="duration" format="integer"></attr> <!--起始點的Y座標--> <attr name="originY" format="integer"></attr> <!--水波紋的高度--> <attr name="waveHeight" format="integer"></attr> <!--水波紋的長度--> <attr name="waveLength" format="integer"></attr> </declare-styleable></resources>
建立一個WaveView類,繼承自View,並初始化一些自訂屬性,這裡兩個重要的屬性一個是一個正弦的最高點,即我們的水波紋的高度;一個是我們一個正弦的長度,即我們一個水波紋的橫座標的長度,下面是一些屬性的初始化 ,很簡單,沒什麼難的
//中間小船圖片的引用 private int imageBitmap; //小船實際的bitmap private Bitmap bitmap; //是否上升水位 private boolean rise; //水位起始點 private int originY; //波紋平移的執行的時間 private int duration; //波紋的寬度 private int waveWidth; //波紋的高度 private int waveHeight; //畫筆 private Paint mPaint; //路徑 private Path mPath; //控制項的寬度高度 private int width; private int height; public WaveView(Context context) { this(context, null); } public WaveView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveView); imageBitmap = a.getResourceId(R.styleable.WaveView_imageBitmap, 0); rise = a.getBoolean(R.styleable.WaveView_rise, false); duration = a.getInt(R.styleable.WaveView_duration, 2000); originY = a.getInt(R.styleable.WaveView_originY, 500); waveWidth = a.getInt(R.styleable.WaveView_waveLength, 500); waveHeight = a.getInt(R.styleable.WaveView_waveHeight, 500); a.recycle(); //壓縮圖片 BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; //壓縮圖片倍數 if (imageBitmap > 0) { bitmap = BitmapFactory.decodeResource(getResources(), imageBitmap,options); } else { bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, options); } //初始化畫筆 mPaint = new Paint(); mPaint.setColor(getResources().getColor(R.color.colorAccent)); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); //初始化路徑 mPath = new Path(); }
然後重寫OnMeasure中測量我們空間的高度,這裡基本上是使用系統測量的寬高度,就是在height為wrap_content的時候設定了800px,這裡的代碼也很簡單,不多解釋,直接上代碼
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); //擷取寬的模式 int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 擷取高的模式 int widthSize = MeasureSpec.getSize(widthMeasureSpec); //擷取寬的尺寸 int heightSize = MeasureSpec.getSize(heightMeasureSpec); //擷取高的尺寸 if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = 800; } //儲存丈量結果 setMeasuredDimension(width, height); }
繼續,重寫OnDraw方法,注意了,這是今天整篇部落格重點的地方,首先我們知道要使用貝塞爾三階來實現,所以我們可以基本上寫出如下的代碼:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //不斷的計算波浪的路徑 calculatePath(); //繪製水部分 canvas.drawPath(mPath, mPaint);}
關鍵是我們calculatePath()方法中的邏輯處理,這是直接使用貝塞爾,首先我們把我們的繪製起始點平移到我們自訂originY屬性的位置
mPath.moveTo(0, originY);
然後在通過我們的width長度和waveHeight的長度來判斷,到底在螢幕中繪製多少個正弦曲線
for (int i = -waveWidth; i < width + waveWidth; i += waveWidth) { //利用三階貝茲路徑繪製 mPath.rCubicTo(????);}
OK,這裡我們繪製整體的思路沒什麼問題了,關鍵我們三階貝茲路徑的兩個控制點和一個結束點的座標的確認了(這裡壓根不知道什麼是控制點和結束點的同學整真的推薦你先去看看我部落格的貝塞爾基礎知識了)
這裡請大家看我在中標註的四個點就分別是我們的起始點、控制點1、控制點2、結束點,ok,所以我們可以寫成如下的代碼:
mPath.moveTo(0, originY); //繪製波浪 for (int i = -waveWidth; i < width + waveWidth; i += waveWidth) { //利用三階貝茲路徑繪製 mPath.rCubicTo(waveWidth / 4, -waveHeight, waveWidth / 4 * 3, waveHeight, waveWidth, 0); }
ok,寫到這裡了我們就可以看一下我們的貝塞爾三階的效果了,如下:
繪製的曲線有點淡,不過還是繪製出來了,但是感覺這裡的三階繪製的曲線和我們想象中的正弦虛線還是有些差距的,我們將三階換成兩個二階試試
mPath.moveTo(0, originY); //繪製波浪 for (int i = -waveWidth; i < width + waveWidth; i += waveWidth) { //利用三階貝茲路徑繪製// mPath.rCubicTo(waveWidth / 4, -waveHeight, waveWidth / 4 * 3, waveHeight, waveWidth, 0); //利用二階貝茲路徑繪製 mPath.rQuadTo(waveWidth / 4, -waveHeight, waveWidth / 2, 0); mPath.rQuadTo(waveWidth / 4, waveHeight, waveWidth / 2, 0); }
如下:
ok,沒問題,這樣的話就和要的效果差不多了,我們繼續要實現下面的水是填充滿的那麼我們還需要繪製一下這三線(黃色的標記的),這樣才能組成一個封閉的地區。
邏輯很簡單,我就直接上代碼了
//繪製連線 mPath.lineTo(width, height); mPath.lineTo(0, height); mPath.close();
再看一下
沒問題,到這裡我們已經成功了我們今天任務的三分之一了,我們接著實現,現在我們想著的是怎麼才能讓我們的水波紋動起來,這裡肯定有同學會說,那肯定屬性動畫啊,對的,沒錯,是使用屬性動畫,但是,怎麼使用?在哪裡使用是一個問題(第一個痛點來了)!!
這裡我想的思路是改變我們繪製波長的起始座標,設定(-waveWidth,originY)為其實座標,為什麼這樣來呢?因為我們打算最左邊多繪製一個波長的水(這裡有個bug,所以也要在最右邊多繪製一個波長,具體解釋看中的標註),然後通過屬性動畫平移(且不但重複平移一個周長的長度),這樣就可以達到我們的動畫效果,
所以代碼修改成了如下:
mPath.moveTo(-waveWidth + dx, originY); for (int i = -waveWidth; i < width + waveWidth; i += waveWidth) { //利用三階貝茲路徑繪製// mPath.rCubicTo(waveWidth / 4, -waveHeight, waveWidth / 4 * 3, waveHeight, waveWidth, 0); //利用二階貝茲路徑繪製 mPath.rQuadTo(waveWidth / 4, -waveHeight, waveWidth / 2, 0); mPath.rQuadTo(waveWidth / 4, waveHeight, waveWidth / 2, 0); } //繪製連線 mPath.lineTo(width, height); mPath.lineTo(0, height); mPath.close();
ok,這樣我們下面在編寫一個簡單的動畫,動態改變dx的值,從而改變我們動畫向右移動(這裡涉及到屬性動畫,不過裡面的知識都是最基礎的,大家應該能看懂)
//開始動畫 public void startAnimation() { animator = ValueAnimator.ofFloat(0, 1); animator.setDuration(duration); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = (float) animation.getAnimatedValue(); dx = (int) (waveWidth * fraction); postInvalidate(); } }); animator.start(); }
ok,在這裡我們就可以看一下我們的動畫效果了,別忘記了在Activity中去調用
mWaveView = (WaveView)findViewById(R.id.waveview); mWaveView.startAnimation();
ok,這樣我們下面的水波紋就搞定了,這樣我們就差不多完成了二分之一了,我們繼續,現在差的就是繪製我們的小船了,先隨便找個點先把小船搞出來,再在後面慢慢的考慮它安放的具體位置,這裡我先寫個固定高度800
protected void onDraw(Canvas canvas) { super.onDraw(canvas); //不斷的計算波浪的路徑 calculatePath(); //繪製水部分 canvas.drawPath(mPath, mPaint); //繪製小船部分 canvas.drawBitmap(bitmap,width/2,800,mPaint); }
看一下效果
圖片倒是展示出來了,現在就是怎麼樣讓他隨著波浪上下滾動,有些同學可能就會說,阿獃哥哥啊 ,很簡單啊,也是很明顯x座標是固定的,就是width的一般,Y座標就是挨著它波浪的高度,直接搞個屬性動畫,隨著波浪高度的改變而改變唄。
恩,關鍵是挨著它的那個波浪的那個座標該怎麼計算,這是問題的關鍵點(這是我們實現這個效果的第二個困痛點)
這裡提供一個思路,我們繪製一條中垂線,即這條藍色的線和每次我們水波紋相交的點就是我們小船圖片的放置點
現在思路清晰了,現在就是要找到這個交點,那麼Android中Path類中有沒有方法是可以拿到這個值得呢? 很明確的告訴你沒有,現在到這裡我們的思路又斷了,但是我告訴大家這裡有一個Region類可以代替的實現這種效果(由於篇幅已經很長了,這就就不和大家詳細介紹Region類的),這個類的解釋就是擷取兩個地區的交集地區,例如:圖下的小矩形地區就是我們大的矩形和水波紋的交集地區
我們按照數學的極限思想來想一下,當這裡我們外面大的矩形地區左右座標無線接近的時候我們矩形就可以看做是一條直線了,這樣就達到了我們之前的要求了
思路就很清晰了,我們來看代碼
float x = width / 2; region = new Region(); Region clip = new Region((int) (x - 0.1), 0, (int) x, height); region.setPath(mPath, clip);
這裡要提醒一下,一定要放在繪製貝茲路徑之後、繪製其它三條線之前(這是一個坑,大家要注意一下)
再看看拿到矩形地區並設定圖片的座標(這裡我直接取得這個矩形的有座標和上座標)
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //不斷的計算波浪的路徑 calculatePath(); //繪製水部分 canvas.drawPath(mPath, mPaint); //擷取當前小船應該在的地方 Rect rect = region.getBounds(); canvas.drawBitmap(bitmap, rect.right, rect.top, mPaint); }
看一下效果
效果大致出來了,可能有些同學說,這是因為bitmap的起始點不是他的中心點,那麼我們繼續修改修改
canvas.drawBitmap(bitmap, rect.right - (bitmap.getWidth()/2), rect.top-(bitmap.getHeight()/2), mPaint);
再看看效果
這時候看起來舒服多了,大致的偏差沒什麼問題了,但是在波穀的時候還是有一點問題,這是什麼原因呢,這裡呢,我們還是有點偏差的,當Y座標大於originY的時候,我們這裡使用rect.bottom拿到的值會更精確一些;當Y座標小於originY的時候,我們這裡使用rect.top拿到的值會更精確一些(大家認真的思考一下,這裡其實很好懂得)
//擷取當前小船應該在的地方 Rect rect = region.getBounds(); Log.i("wangjitao", "right:" + rect.right + ",top:" + rect.bottom); if (rect.top < originY){ canvas.drawBitmap(bitmap, rect.right - (bitmap.getWidth()/2), rect.top-(bitmap.getHeight()/2), mPaint); }else { canvas.drawBitmap(bitmap, rect.right - (bitmap.getWidth()/2), rect.bottom-(bitmap.getHeight()/2), mPaint); }
效果如下:
ok,現在我們的座標就完全正確了,沒問題了,搞定
其實這裡還有更好擴充的小效果,如下:
1,提供剛進來的時候漲水效果2,船水波紋飄動的時候,船的方向也隨著波紋的切線平行(這裡就要使用到sin 的求導,可以我忘記完了)
這些功能在這裡就不和大家實現了,大家可以下去自己實現,今天有晚了,不過乾貨還是挺多的,希望大家好好理解,特別是我們遇到問題時候該怎麼解決,這個很關鍵。不多說了,睡覺了。See You Next Time.........
Android -- 貝塞爾實現水波紋動畫(劃重點!!)