拆輪子-唯美細膩的夕陽海浪動畫

mantxu 8年前發布 | 8K 次閱讀 Bitmap Android開發 移動開發

今天帶來的是一個博主有史以來見過最最精美的動畫效果了。所以我才迫不及待的拆輪子。今天拆的部分是如下的效果。

無圖無真相呀 

前人種樹,后人乘涼。很早的時候大神CJJ關于這個庫的一些分析,大家可以去看看。

不過由于該文章中對于動畫的具體實現沒有提及,所以才有這篇文章的存在了=.=

大家如果之前接觸過類似的效果,其實也就沒有必要看下去啦=.=

好了,廢話說了這么多,如果有興趣往下看,就繼續吧!

一.說明

因為代碼已經存在,所以我認為就沒有必要照著代碼一點點講,感覺這樣的方式更容易把讀者弄得云里霧里的,所以我這里決定從零開始一步步來實現這個效果(當然因為我是先仔細看了一遍代碼的,所以你懂的)

這個地址是本文的代碼(基本都是從原來的庫取出來的,只不過精簡了些),大家閱讀前也可以去下載看看。 

二.分析

通過上面的效果圖,我們看到我們要實現的這樣的效果需要攻破幾個難關

1.水的背景波浪是浪起來的? 2.水面上的波紋是如何看起來隨機產生并且粗細不同的?

答1:這里的背景波浪不同平常于我們經常看到別人發的用正弦函數+三次貝塞爾曲線形成的這種效果

我們可以注意到整個背景有一種被扭曲的效果,背景的色彩呈現擠壓然后釋放然后擠壓這種效果。

想要實現我們的這種效果,我們這里需要一個我們較少使用的方法 canvas.drawBitmapMesh

答2:對于第二個問題,我想留在后面適當的位置再闡述。

知識點 drawBitmapMesh 補充

我就提煉出一些精要的東西好了

好了,假定你已經知道drawBitmapMesh的大致用法了。

三.實現

有了上面的分析,我們就開始一步步來實現吧 整個項目的工程如下

3.1先來介紹Renderable

可以看到Renderable很簡單。我們的Water類繼承自Renderable

3.2 Water類

然后我們創建Water類,用來承載整個海浪的效果(背景大波浪+波紋)

3.2.1 實現背景大波浪的效果

為了避免混淆,我把實現波紋的效果給注釋掉了,

public class Water extends Renderable {

    private float mWidth;
    private float mHeight;
    private PathBitmapMesh mWaterMesh;
    private float mWaveHeight;
    private Path mWaterPath = new Path();
    private int mNumWaves;
    /*private Foam[] foams = new Foam[1];
    long lastEmit;
    private int emitInterWall = 1000;*/

    /**
     *
     * @param water water圖像
     * @param foam 海浪圖像
     * @param y 海浪起始左上角坐標的y值
     * @param width 海浪顯示的寬度
     * @param height 海浪顯示的高度
     * @param numWaves 海浪整個寬度被分成多少份
     */
    public Water(Bitmap water, Bitmap foam, float y, float width, float height, int numWaves) {
        super(water, 0, y);
        mWidth = width;
        mHeight = height;
        mWaterMesh = new PathBitmapMesh(water, 1500);
        mWaveHeight = height / 20;
        mNumWaves = numWaves;
        /*foams[0] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, 0, height / 12, 1500);
        foams[1] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 5, height / 5, 1500);
        foams[1].setAlpha(100);
        foams[2] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 12, height / 12, 1450);
        foams[2].setVerticalOffset(height / 7);
        foams[3] = new Foam(PathBitmapMesh.HORIZONTAL_COUNT, foam, -height / 12, height / 12, 1400);
        foams[3].setVerticalOffset(height / 4);
        lastEmit = System.currentTimeMillis();*/
        createPath();
    }

    private void createPath() {
        mWaterPath.reset();
        mWaterPath.moveTo(0, y);
        int step = (int) (mWidth / mNumWaves);
        boolean changeDirection = true;
        for (int i = 0; i < mNumWaves; i++) {
            if (changeDirection) {
                mWaterPath.cubicTo(x + step * i, y, x + step * i + step / 2f, y + mWaveHeight, x + step * i + step, y);
            } else {
                mWaterPath.cubicTo(x + step * i, y, x + step * i + step / 2f, y - mWaveHeight, x + step * i + step, y);
            }
            changeDirection = !changeDirection;
        }
    }

    @Override
    public void draw(Canvas canvas) {
        mWaterMesh.draw(canvas);
        /*for (Foam foam : foams) {
            foam.draw(canvas);
        }*/
    }

    @Override
    public void update(float deltaTime) {
        mWaterMesh.matchVertsToPath(mWaterPath, mHeight, ((bitmap.getWidth() / mNumWaves) * 4f));
        /*for (Foam foam : foams) {
            foam.update(deltaTime);
        }
        for (Foam foam : foams) {
            foam.matchVertsToPath(mWaterPath, ((foam.getBitmap().getWidth() / mNumWaves) * 4f));
        }
        if (lastEmit + emitInterWall < System.currentTimeMillis()) {
            for (Foam foam : foams) {
                foam.calcWave();
            }
            lastEmit = System.currentTimeMillis();
        }*/
    }
}

先介紹createPath這個方法,這里面利用cubicTo創建了一個三次貝塞爾曲線,目的就是:我們知道圖像扭曲時的寬度是大于二維圖像平面的寬度的。 所以我們這里一條多個三次貝塞爾曲線形成的曲線來模擬圖像扭曲時的寬度 這樣說比較抽象,大致想表達的就是紅色來模擬圖像扭曲時的長度

createPath這個方法中有個有趣的參數mNumWaves,這個用來表示海浪整個寬度被分成多少份(這個例子中我們把海浪分成了6份)。

那我們就來分析我們的背景大波浪的形成辦法。

按照上面drawBitmapMesh的講解,我們要想形成大波浪,就需要計算出Mesh格子各個頂點的坐標(事實上這也是最最關鍵的一步,知道了這一步后面的都是小菜!) 這里我們把海浪寬度分成了6份,高度分成了1份來處理。所以我們需要處理的頂點個數就是 (6+1)*(1+1) = 14(圖中14個黑點)

然后我們接下來的工作就是計算著14個點的坐標!這里就是PathBitmapMesh類需要完成的工作了。 (注釋部分先忽略,后面再說)

public class PathBitmapMesh {

    protected static int HORIZONTAL_COUNT = 6;//水平方向分片數
    protected static int VERTICAL_COUNT = 1;//垂直方向分片
    private int mTotalCount;//總共需要計算的網格頂點個數
    protected Bitmap bitmap;
    protected float[] drawingVerts;//需要繪制的Verts網格坐標
    protected float[] staticVerts;//最初始的Verts網格坐標
    private Paint mPaint = new Paint();
    //private ValueAnimator mValueAnimator;
    //protected float pathOffsetPercent;
    protected float[] coordsX = new float[2];
    protected float[] coordsY = new float[2];

    public PathBitmapMesh(Bitmap bitmap, long duration) {
        mTotalCount = (HORIZONTAL_COUNT + 1) * (VERTICAL_COUNT + 1);
        drawingVerts = new float[mTotalCount * 2];
        staticVerts = new float[mTotalCount * 2];
        this.bitmap = bitmap;
        initVert();
        //startValuesAnim(duration);
    }

    /*private void startValuesAnim(long duration) {
        mValueAnimator = ValueAnimator.ofFloat(0, 0.3334f);
        mValueAnimator.setDuration(duration);
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
        mValueAnimator.setInterpolator(new LinearInterpolator());
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                pathOffsetPercent = (float) animation.getAnimatedValue();
            }
        });
        mValueAnimator.start();
    }*/

    private void initVert() {
        float bitmapWidth = (float) bitmap.getWidth();
        float bitmapHeight = (float) bitmap.getHeight();
        int index = 0;
        for (int y = 0; y <= VERTICAL_COUNT; y++) {
            float fy = bitmapHeight / VERTICAL_COUNT * y;
            for (int x = 0; x <= HORIZONTAL_COUNT; x++) {
                float fx = bitmapWidth / HORIZONTAL_COUNT * x;
                setXY(drawingVerts, index, fx, fy);
                setXY(staticVerts, index, fx, fy);
                index++;
            }
        }
    }

    protected void setXY(float[] arrys, int index, float x, float y) {
        arrys[2 * index] = x;
        arrys[2 * index + 1] = y;
    }

    public void matchVertsToPath(Path path, float bottomY, float extraOffset) {
        PathMeasure pm = new PathMeasure(path, false);
        for (int i = 0; i < staticVerts.length / 2; i++) {
            float orignX = staticVerts[2 * i];
            float orignY = staticVerts[2 * i + 1];
            float percentOffsetX = orignX / bitmap.getWidth();
            float percentOffsetY = orignX / (bitmap.getWidth() + extraOffset);
            //percentOffsetY += pathOffsetPercent;
            pm.getPosTan(pm.getLength() * (percentOffsetX), coordsX, null);
            pm.getPosTan(pm.getLength() * (percentOffsetY), coordsY, null);
            if (orignY == 0) {
                setXY(drawingVerts, i, coordsX[0], coordsY[1]);
            } else {
                setXY(drawingVerts, i, coordsX[0], bottomY);
            }
        }
    }

    public void draw(Canvas canvas) {
        canvas.drawBitmapMesh(bitmap, HORIZONTAL_COUNT, VERTICAL_COUNT, drawingVerts, 0, null, 0, mPaint);
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }
}

可以看到initVert()進行了drawingVerts,staticVerts兩個記錄頂點坐標數組初始化工作。 draw方法中直接調用drawBitmapMesh完成扭曲圖像的繪制。

重點在于matchVertsToPath方法!

上面說過了,我們用一條多個三次貝塞爾曲線形成的曲線來模擬圖像扭曲時的寬度。那我們怎么來取得這條曲線上任一點的坐標呢?

答案就是PathMeasure。我們可以通過該函數的getPosTan方法,通過給定曲線的某一長度,得到該點的坐標。

例如:pm.getPosTan(pm.getLength() * (percentOffsetX), coordsX, null);

pm.getLength()得到整條曲線長度,percentOffsetX得到曲線上比例值,最后把得到的坐標返回給coordsX數組。

假設我們的bitmap寬度為60,這里我們把整個圖像分成了6份,以A點舉例。 percentOffsetX = 1/6;

因為我們在繪制path時是以整個圖像顯示的寬度作為標準的,而不是bitmap的寬度。 所以我們的path長度不等于bitmap的寬度,這也就是為什么bitmap的寬度很小,卻能鋪滿整個屏幕的寬度

如果path的長度為600,那么A的y值也就是coordsX[0] = 600*1/6=100 通過這樣的方法我們就可以得到14個頂點的坐標

細心的朋友會發現,那這樣我們有一個percentOffsetX也好了呀,為啥需要percentOffsetY? (percentOffsetX字面理解為X方向偏移占據比例) 如果我們試著把extraOffset給注釋掉

那么運行的效果就是

你也許會想不會呀,明明我們的path是三次貝塞爾曲線,不是彎的嗎?怎么直了!

原因就是:path只是用來模擬圖像扭曲時寬度,并不是真正的形狀! 按照我們之前的計算結果A,B點的坐標y值都一樣,所以整個圖像看起來效果是直的。

而如果我們加上extraOffset,用藍色點的y值來表示A,B 坐標的y值,那么就會出現這樣的效果了!

那我們怎么讓我們的波浪動起來呢?

很簡單!讓A,B第一行的七個坐標y值不斷變化即可! 創建一個ValueAnimator,讓其循環周期的變化即可,具體大家可以看之前代碼注釋部分

為什么是0-1/3f呢? 很簡單,還記得我們設置的extraOffset=((bitmap.getWidth() / mNumWaves) * 4f);

也就是extraOffset = w/6*4 = 2*w/3; (w表示bitmap.getWidth())

那么代入計算得到A點的percentOffsetY = 1/(w+2 w/3)=3 w/5(也就是A’的y位置)

我們要想讓波浪形成一個周期,很明顯,讓A’運動至A”即可 得到A”-A’=w/3 所以ValueAnimator.ofFloat(0, 1 / 3f)

進行到,大波浪終于算是完成了!

拆輪子-唯美細膩的夕陽海浪動畫

3.2.1 實現水紋效果

有了前面大波浪的經驗,我們實現水紋應該更簡單了!因為原理類似呀! 首先創建一個類Foam 繼承自 PathBitmapMesh (意味著我們可以復用PathBitmapMesh里面的東西)

public class Foam extends PathBitmapMesh {

    private float[] foamCoords;
    private float[] easedFoamCoords;
    private int mHorizontalSlices;//水紋水平方向分片
    private float minHeight;//水紋最小高度
    private float maxHeight;//水紋最大高度
    private float verticalOffset;

    public Foam(int horizontalSlices, Bitmap bitmap, float minHeight, float maxHeight, long duration) {
        super(bitmap, duration);
        mHorizontalSlices = horizontalSlices;
        this.minHeight = minHeight;
        this.maxHeight = maxHeight;
        init();
    }

    private void init() {
        foamCoords = new float[mHorizontalSlices];
        easedFoamCoords = new float[mHorizontalSlices];
        for (int i = 0; i < mHorizontalSlices; i++) {
            foamCoords[i] = 0;
            easedFoamCoords[i] = 0;
        }
    }

    /**
     * 隨著時間的流逝不斷更改
     * @param deltaTime
     */
    public void update(float deltaTime) {
        for (int i = 0; i < foamCoords.length; i++) {
            easedFoamCoords[i] += ((foamCoords[i] - easedFoamCoords[i])) * deltaTime;
        }
    }

    /**
     * 根據傳入的最低,最高高度得到一個適合的高度
     */
    public void calcWave() {
        for (int i = 0; i < foamCoords.length; i++) {
            foamCoords[i] = MathHelper.randomRange(minHeight, maxHeight);
        }
    }

    /**
     * 計算水紋的各個頂點坐標
     * @param path
     * @param extraOffset
     */
    public void matchVertsToPath(Path path, float extraOffset) {
        PathMeasure pm = new PathMeasure(path, false);
        int index = 0;
        for (int i = 0; i < staticVerts.length / 2; i++) {
            float orignX = staticVerts[2 * i];
            float orignY = staticVerts[2 * i + 1];
            float percentOffsetX = orignX / bitmap.getWidth();
            float percentOffsetY = orignX / (bitmap.getWidth() + extraOffset);
            percentOffsetY += pathOffsetPercent;
            pm.getPosTan(pm.getLength() * percentOffsetX, coordsX, null);
            pm.getPosTan(pm.getLength() * percentOffsetY, coordsY, null);
            if (orignY == 0) {
                setXY(drawingVerts, i, coordsX[0], coordsY[1]+verticalOffset);
            } else {
                float desiredYCoord = Math.max(coordsY[1], coordsY[1] + easedFoamCoords[Math.min(easedFoamCoords.length - 1, index)]);
                setXY(drawingVerts, i, coordsX[0], desiredYCoord+verticalOffset);
                index += 1;
            }
        }
    }

    public void setVerticalOffset(float verticalOffset) {
        this.verticalOffset = verticalOffset;
    }
}

還是重點來看matchVertsToPath方法

這里我畫了一個水紋的簡圖(藍色的線所圍成)

可以看到對于第一排坐標就是跟隨者大波浪的坐標即可

我們需要改變的是第二排的坐標y值,這里就根據我們傳入的minHeight,maxHeight計算所得(具體計算方式也好理解,可以看代碼,我們實際上的計算方式可以不按照原代碼的,只需要y值不一樣即可) 參量verticalOffset我們可以設定水紋的起始高度,這樣通過修改verticalOffset值,我們可以讓多個水紋在垂直高度不同位置顯示。 最后實現的效果就是(把Water代碼注釋部分去掉)因為圖像就是扭曲的,水紋給我們的感覺就像是隨機產生,又水紋每一份頂點的y值不同,所以就粗細不同了。

注意,因為我們的大波浪頂點的高度是需要時刻變化的,而水紋的高度并不需要時刻變化,只需要每隔一段時間變化即可,所以我們可以設置讓水紋的高度每隔1秒計算變化一次

四.合成

通過上面的分析,我們已經成功掌握了大波浪+水紋實現的原理,最后再創建WaterScreenView類,把背景山和太陽的光輝加上,就完美實現了!

public class WaterScreenView extends View {

    private Water mWater;
    private Renderable[] mRenderables;

    public WaterScreenView(Context context) {
        super(context);
    }

    public WaterScreenView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public WaterScreenView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mRenderables == null && getWidth() != 0) {
            init();
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mRenderables == null) {
            init();
        }
    }

    private void init() {
        mRenderables = new Renderable[2];
        Bitmap waterBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.water);
        Bitmap foam = BitmapFactory.decodeResource(getResources(), R.drawable.foam);
        setLayerType(View.LAYER_TYPE_HARDWARE, null);
        mWater = new Water(waterBitmap, foam, getHeight() * 0.65f, getWidth(), getHeight(), 6);
        mRenderables[0] = mWater;
        Bitmap aura = BitmapFactory.decodeResource(getResources(), R.drawable.sun_aura);
        mRenderables[1] = new Renderable(aura, getWidth() * 0.5f, getHeight() * 0.35f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float deltaTime = FrameRateCounter.timeStep();

        for (Renderable renderable : mRenderables) {
            renderable.draw(canvas);
            renderable.update(deltaTime);
        }
        if (!isPause) {
            invalidate();
        }
    }

    private boolean isPause = false;
}

最終效果:

五.結尾

嘩啦啦的寫了這么多,畫圖好累呀。原效果中的噪聲效果還沒有來得及分享,一篇文章內容太多就太亂了。 總而言之,就是好好利用drawBitmapMesh這個牛逼的東東吧 有機會分享給大家怎么利用drawBitmapMesh讓這個圖像的屁股扭動起來!

嗯,我就是ImmortalZ,一個Android小菜鳥,歡迎大家一起學習。

 

 

來自:http://www.androidchina.net/5789.html

 

 本文由用戶 mantxu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!