Android 窗簾(Curtain)效果四之賽貝爾曲線優化

weixin_33850890發表於2018-09-08

Github原始碼

下一篇:Android 窗簾(Curtain Menu)效果五之應用場景和使用方法

經過 上一篇文章的優化後我們最終實現的應用效果自我感覺已經不錯了。但是仔細再滑動了幾下又發下好幾處問題,琢磨了下發現這坑有點深啊,正弦曲線優化貌似行不通了;在排除手勢拖拽效果的前提下,我們配合如下實際的應用效果圖找出問題點:

自己實現特效 Morning Routine
10954945-51786dfc9fc12990.gif
QQ圖片20180827232654.gif
10954945-23915804b5106234.gif
QQ圖片20180827232654.gif

問題點:

(1).效果圖的最下邊好像被擠出去了,沒看到皺褶空隙(上一篇文章已經解決)
(2).左邊皺褶的波浪間距是均勻分佈的,右邊波浪間距是從左往右遞減的

這裡裡我們要對第二點進行優化,糾結了好久,看到這篇文章Android 使用貝塞爾曲線將多點連成一條平滑的曲線靈感就來了,這篇文章的要點主要是三階賽貝爾曲線的控制點計算,和PathMeasure的使用,這裡就不介紹了。我把程式碼clone下來修改了下實現了一個不均勻分佈的波浪曲線,如下圖1

10954945-1d81e18882ce71a1.gif
QQ圖片20180908032448.gif

綠色線條上邊的紅點是我們繪製平滑賽貝爾曲線的頂點,我通過為每個頂點之間設定不同的水平距離和動態控制頂點之間的垂直距離,使得綠色線條從直線扭曲成不均勻波浪賽貝爾曲線,這正是我想要的結果。附上這個Demo原始碼

原理先不寫,有空再加吧,下面就是我通過賽貝爾曲線對水平/豎直畫素優化畫素的結果:


10954945-edeac7413b83dc92.gif
QQ圖片20180908035530.gif

完整程式碼:

public class CurtainView extends View {
    private Bitmap mbitmap;
    private static int WIDTH = 30;
    private static int HEIGHT = 30;
    //最大垂直的波形高度
    private static float V_MAX_WAVE_HEIGHT = 450;
    //最大垂直的波形高度
    private static float H_MAX_WAVE_HEIGHT = 50;

    //小格相交的總的點數
    private int COUNT = (WIDTH + 1) * (HEIGHT + 1);
    private float[] verts = new float[COUNT * 2];
    private float[] origs = new float[COUNT * 2];
    private int[] colors = new int[COUNT * 2];
    private float k;
    private float progress;

    private int bitmapwidth;
    private int bitmapheight;

    private List<Point> points;
    private float[] pos = new float[2];
    private PathMeasure pathMeasure;

    private float[] xOffsets = new float[WIDTH + 1];
    private float[] yOffsets = new float[WIDTH + 1];

    float[] waveTops = {0, 0.03F, 0.08F, 0.15F, 0.24F, 0.36F, 0.49F, 0.64F, 0.81F, 1.0F};

    public CurtainView(Context context) {
        super(context);
        init();
    }

    public CurtainView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CurtainView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public void init() {
        mbitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.timg);
        bitmapwidth = mbitmap.getWidth();
        bitmapheight = mbitmap.getHeight();

        points = new ArrayList<>();
        pathMeasure = new PathMeasure();

        COUNT = (WIDTH + 1) * (HEIGHT + 1);
        verts = new float[COUNT * 2];
        origs = new float[COUNT * 2];

        int index = 0;
        for (int i = 0; i < HEIGHT + 1; i++) {
            float fy = bitmapheight / (float) HEIGHT * i;
            for (int j = 0; j < WIDTH + 1; j++) {
                float fx = bitmapwidth / (float) WIDTH * j;
                //偶數位記錄x座標  奇數位記錄Y座標
                origs[index * 2] = verts[index * 2] = fx;
                origs[index * 2 + 1] = verts[index * 2 + 1] = fy;
                index++;
            }
        }

        for (int i = 0; i < waveTops.length; i++) {
            Point point = new Point();
            point.x = (int) Math.floor((double) (bitmapwidth * waveTops[i]));
            point.y = i % 2 == 0 ? 0 : (int) (H_MAX_WAVE_HEIGHT * progress);
            points.add(point);
        }

        BezierUtils.measurePath(pathMeasure, points);
    }

    public void setProgress(float progress) {
        this.progress = progress;
        for (int t = 0; t < waveTops.length; t++) {
            Point point = points.get(t);
            point.y = t % 2 == 0 ? 0 : (int) (H_MAX_WAVE_HEIGHT * progress);
        }
        BezierUtils.measurePath(pathMeasure, points);
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        for (int j = 0; j < WIDTH + 1; j++) {
            pathMeasure.getPosTan(pathMeasure.getLength() * j / (float) WIDTH, pos, null);
            xOffsets[j] = pos[0];
            yOffsets[j] = pos[1];
        }

        int index = 0;
        for (int i = 0; i < HEIGHT + 1; i++) {
            for (int j = 0; j < WIDTH + 1; j++) {

                //把每一個水平畫素通過正弦公式轉換成正弦曲線
                //H_MAX_WAVE_HEIGHT表示波峰跟波低的垂直距離,皺褶後會王桑超過水平線,所以往下偏移WAVE_HEIGHT / 2
                //5表示波浪的密集度,表示波峰波谷總共有五個,對應上面左圖的1,2,3,4,5
                //j就是水平像的X軸座標
                //K決定正弦曲線起始點(x=0)點的Y座標,k=0就是從波峰波谷的中間開始左->右繪製曲線
                float yOffset = H_MAX_WAVE_HEIGHT / 2 * progress + H_MAX_WAVE_HEIGHT / 2 * progress * (float) Math.sin((float) j / WIDTH * 5 * Math.PI + k);

                yOffset = yOffsets[j];

                //垂直方向豎直壓縮時的座標
                float xPostion = origs[(i * (WIDTH + 1) + j) * 2] + (bitmapwidth - origs[(i * (WIDTH + 1) + j) * 2]) * progress;

                xPostion = xOffsets[j] + (bitmapwidth - xOffsets[j]) * progress;

                //垂直方向正弦曲線優化後的座標,1.1->個波峰波谷
                float vXSinPostion = V_MAX_WAVE_HEIGHT / 2 * progress * (float) Math.sin((float) i / WIDTH * 1.1 * Math.PI + k);
                //每個畫素扭曲後的x座標
                //origs[(i*(WIDTH+1)+j)*2+0] 原圖x座標
                verts[(i * (WIDTH + 1) + j) * 2] = vXSinPostion * ((bitmapwidth - xPostion) / bitmapwidth) + xPostion;
                //每個畫素扭曲後的Y座標
                //origs[(i*(WIDTH+1)+j)*2+1] 原圖y座標
                verts[(i * (WIDTH + 1) + j) * 2 + 1] = origs[(i * (WIDTH + 1) + j) * 2 + 1] + yOffset;//

                int channel = 255 - (int) (yOffset * 3);
                channel = channel < 0 ? 0 : channel;
                channel = channel > 255 ? 255 : channel;
                colors[index] = 0xFF000000 | channel << 16 | channel << 8 | channel;
                index += 1;
            }
        }

        canvas.drawBitmapMesh(mbitmap, WIDTH, HEIGHT, verts, 0, colors, 0, null);
    }
}

下一篇:Android 窗簾(Curtain Menu)效果五之應用場景和使用方法

相關文章