Android進階系列:八、自定義View之音訊抖動動效

Android丶SE開發發表於2019-04-28

自定義動畫效果——音訊抖動效果

1.繪製一個矩形:

想要繪製一個矩形,繼承View,並重寫onDraw方法即可。複雜一點還可以重寫onMeasure方法和onLayout方法進行大小測量和位置測量。但本文不打算寫那麼複雜的view,故只需要重寫一個onDraw方法即可:

 private RectF rectF = new RectF();//繪製矩形
    private float lineWidth = 50;
    private Paint paint = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設定顏色
        paint.setColor(context.getColor(R.color.colorPrimary));
        //填充內部
        paint.setStyle(Paint.Style.FILL);
        //設定抗鋸齒
        paint.setAntiAlias(true);
        //繪製矩形的四邊
        int widthCentre = getWidth() / 2;
        int heightCentre = getHeight() / 2;
        rectF.left = widthCentre - lineWidth / 2;
        rectF.right = widthCentre + lineWidth / 2;
        rectF.top = heightCentre - lineWidth * 2;
        rectF.bottom = heightCentre + lineWidth * 2;
        //繪製圓角矩形,rx:x方向上的圓角半徑。ry:y方向上的圓角半徑。
        canvas.drawRoundRect(rectF, 6, 6, paint);
    }
複製程式碼

1.我們需要初始化一個RectF來繪製矩形,這個類通過一個邊的來繪製矩形。並初始化一個畫筆,和矩形的寬度。

2.在onDraw方法中,設定畫筆paint,包括顏色,填充方式,是否抗拒性。還有更多的設定,讀者可以自行查閱API

3.獲取該View的實際寬高的一半,然後設定矩形的四邊,熟悉Android的view的繪製都知道,view的寬為right - left,高度為bottom - top。所以讓right比left多一個lineWidth即可讓矩形的寬為lineWidth,bottom比top多4

lineWidth即可讓高讀為4
lineWidth,並利用實際寬高的一半,把矩形繪製在view的中央。

4.在畫布上使用drawRoundRect方法繪製一個圓角的矩形

5.然後在xml檔案中引用這個view就可以使用了:

    <com.ycm.customview.LineWaveVoiceView
        android:id="@+id/line1"
        android:layout_width="300dp"
        android:layout_height="100dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        />
複製程式碼

這樣就可以在view中繪製一個矩形,如圖所示:


Android進階系列:八、自定義View之音訊抖動動效

2.繪製多個矩形

現在我們可以繪製多個矩形在畫布上。直接採用for迴圈是不行的,這樣會讓矩形重疊在一起,導致只顯示一個矩形,所以應該控制讓矩形錯開顯示,我們可以讓矩形之間間隔一個lineWidth。如圖所示:


Android進階系列:八、自定義View之音訊抖動動效

我們以第一個矩形的左邊為參照,標為0,則其右邊為1,第二個矩形的左邊為2,右邊為3,以此類推,它們的距離都是lineWidth。所以我們可以得出:

  • 第一個矩形的左邊是0,第二個矩形的左邊是2,第三個左邊是4,以此類推是一個2(n-1)的等差數列
  • 第一個矩形的右邊是1,第二個矩形的右邊是3,第三個右邊是5,以此類推是一個2n-1的等差數列
    所以我們可以這樣寫:
 for (int i = 1; i <= 4; i++) {
            rectF.left = widthCentre - lineWidth / 2 + 2 * (i - 1) * lineWidth;
            rectF.right = widthCentre + lineWidth / 2 + (2 * i - 1) * lineWidth;
            rectF.top = heightCentre - lineWidth * 2;
            rectF.bottom = heightCentre + lineWidth * 2;
            //繪製圓角矩形,rxx方向上的圓角半徑。ryy方向上的圓角半徑。
            canvas.drawRoundRect(rectF, 6, 6, paint);
        }
複製程式碼

當然注意要在迴圈裡繪製圓角矩形,因為繪製多個矩形,當然要有一個繪製一個,不然放到循壞外只能繪製最後一個。效果如圖:


Android進階系列:八、自定義View之音訊抖動動效

3.繪製矩形抖動

我們要繪製音訊抖動的效果,矩形的高度肯定不能一樣,而是要根據聲音的大小來顯示,這裡我們沒有聲音,簡單模擬一下給高度乘上for迴圈裡的i效果如圖:


Android進階系列:八、自定義View之音訊抖動動效

至此我們已經知道了如何繪製多個矩形,並控制不同的高度,那我們要如何動態的控制高度呢?比如我們點選開始錄音的時候,就會動態的傳入聲音的大小,這個分貝值控制著矩形的抖動。要實現這個動態的效果,我們需要不斷的設定分貝,並不斷的重新整理。所以我們可以開啟一個執行緒,不斷設定音量的分貝,並不斷的重新整理。為了讓矩形抖動有錯落感,就需要讓每個矩形抖動的值不一樣,所以我們設定一個list儲存音量值,並依次改變裡面的值即可。

  private static final int MIN_WAVE_HEIGHT = 2;//矩形線最小高
    private static final int MAX_WAVE_HEIGHT = 12;//矩形線最大高
    private static final int[] DEFAULT_WAVE_HEIGHT = {2, 2, 2, 2};
    private static final int UPDATE_INTERVAL_TIME = 100;//100ms更新一次
    private LinkedList<Integer> mWaveList = new LinkedList<>();
    private float maxDb;

    private void resetView(List<Integer> list, int[] array) {
        list.clear();
        for (int anArray : array) {
            list.add(anArray);
        }
    }

    private synchronized void refreshElement() {
        Random random = new Random();
        maxDb = random.nextInt(5) + 2;
        int waveH = MIN_WAVE_HEIGHT + Math.round(maxDb * (MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT));
        mWaveList.add(0, waveH);
        mWaveList.removeLast();
    }

    public boolean isStart = false;

    private class LineJitterTask implements Runnable {
        @Override
        public void run() {
            while (isStart) {
                refreshElement();
                try {
                    Thread.sleep(updateSpeed);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                postInvalidate();
            }
        }
    }

    public synchronized void startRecord() {
        isStart = true;
        executorService.execute(task);
    }

    public synchronized void stopRecord() {
        isStart = false;
        mWaveList.clear();
        resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
        postInvalidate();
    }
複製程式碼

1.為了控制矩形抖動的範圍,我們需要設定一個最大值和最小值。
2.並利用陣列設定矩形的預設值,因為有四個矩形,所以陣列大小為4
3.定義一個分貝值,控制矩形的高度
4.重置View的時候把預設的陣列傳進去,就可以達到View的重置,比如View的初始化,和停止錄音的時候
5.重新整理元素方法,用於不停的重新整理矩陣的高度,讓矩陣抖起來。這裡用隨機數模擬聲音大小,傳給陣列,每次都新增到第一個,然後每次都移除最後一個,這樣能讓矩陣按順序抖動。
6.線上程中呼叫這個重新整理矩陣的方法,當開始錄音的時候,在while中重新整理矩陣,並睡眠100ms,這樣就實現了沒100ms重新整理一次view,開始錄音的時候設定isStart為true。
7.在停止錄音的時候設定isStart為false,並初始化矩形為原始高度。由於線上程中重新整理View,應該使用postInvalidate()方法。

至此這個邏輯已經實現了,稍微潤色一下即可實現錄音時的音訊抖動
效果如圖:


Android進階系列:八、自定義View之音訊抖動動效
完整程式碼:
/**
 * 語音錄製的動畫效果
 */
public class LineWaveVoiceView extends View {
    private static final String DEFAULT_TEXT = " 請錄音 ";
    private static final int LINE_WIDTH = 9;//預設矩形波紋的寬度,9畫素, 原則上從layout的attr獲得
    private Paint paint = new Paint();
    private Runnable task;
    private ExecutorService executorService = Executors.newCachedThreadPool();
    private RectF rectRight = new RectF();//右邊波紋矩形的資料,10個矩形複用一個rectF
    private RectF rectLeft = new RectF();//左邊波紋矩形的資料
    private String text = DEFAULT_TEXT;
    private int updateSpeed;
    private int lineColor;
    private int textColor;
    private float lineWidth;
    private float textSize;


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

    public LineWaveVoiceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LineWaveVoiceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(attrs, context);
        resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
        task = new LineJitterTask();
    }

    private void initView(AttributeSet attrs, Context context) {
        //獲取佈局屬性裡的值
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.LineWaveVoiceView);
        lineColor = mTypedArray.getColor(R.styleable.LineWaveVoiceView_voiceLineColor, context.getColor(R.color.defaultLineColor));
        lineWidth = mTypedArray.getDimension(R.styleable.LineWaveVoiceView_voiceLineWidth, LINE_WIDTH);
        textSize = mTypedArray.getDimension(R.styleable.LineWaveVoiceView_voiceTextSize, 42);
        textColor = mTypedArray.getColor(R.styleable.LineWaveVoiceView_voiceTextColor, context.getColor(R.color.defaultTextColor));
        updateSpeed = mTypedArray.getColor(R.styleable.LineWaveVoiceView_updateSpeed, UPDATE_INTERVAL_TIME);
        mTypedArray.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //獲取實際寬高的一半
        int widthCentre = getWidth() / 2;
        int heightCentre = getHeight() / 2;
        paint.setStrokeWidth(0);
        paint.setColor(textColor);
        paint.setTextSize(textSize);
        float textWidth = paint.measureText(text);
        canvas.drawText(text, widthCentre - textWidth / 2, heightCentre - (paint.ascent() + paint.descent()) / 2, paint);

        //設定顏色
        paint.setColor(lineColor);
        //填充內部
        paint.setStyle(Paint.Style.FILL);
        //設定抗鋸齒
        paint.setAntiAlias(true);
        for (int i = 0; i < 10; i++) {
            rectRight.left = widthCentre + textWidth / 2 + (1 + 2 * i) * lineWidth;
            rectRight.top = heightCentre - lineWidth * mWaveList.get(i) / 2;
            rectRight.right = widthCentre + textWidth / 2 + (2 + 2 * i) * lineWidth;
            rectRight.bottom = heightCentre + lineWidth * mWaveList.get(i) / 2;

            //左邊矩形
            rectLeft.left = widthCentre - textWidth / 2 - (2 + 2 * i) * lineWidth;
            rectLeft.top = heightCentre - mWaveList.get(i) * lineWidth / 2;
            rectLeft.right = widthCentre - textWidth / 2 - (1 + 2 * i) * lineWidth;
            rectLeft.bottom = heightCentre + mWaveList.get(i) * lineWidth / 2;

            canvas.drawRoundRect(rectRight, 6, 6, paint);
            canvas.drawRoundRect(rectLeft, 6, 6, paint);
        }
    }

    private static final int MIN_WAVE_HEIGHT = 2;//矩形線最小高
    private static final int MAX_WAVE_HEIGHT = 12;//矩形線最大高
    private static final int[] DEFAULT_WAVE_HEIGHT = {2,2, 2, 2,2, 2, 2, 2,2,2};
    private static final int UPDATE_INTERVAL_TIME = 100;//100ms更新一次
    private LinkedList<Integer> mWaveList = new LinkedList<>();
    private float maxDb;

    private void resetView(List<Integer> list, int[] array) {
        list.clear();
        for (int anArray : array) {
            list.add(anArray);
        }
    }

    private synchronized void refreshElement() {
        Random random = new Random();
        maxDb = random.nextInt(5) + 2;
        int waveH = MIN_WAVE_HEIGHT + Math.round(maxDb * (MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT));
        mWaveList.add(0, waveH);
        mWaveList.removeLast();
    }

    public boolean isStart = false;

    private class LineJitterTask implements Runnable {
        @Override
        public void run() {
            while (isStart) {
                refreshElement();
                try {
                    Thread.sleep(updateSpeed);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                postInvalidate();
            }
        }
    }

    public synchronized void startRecord() {
        isStart = true;
        executorService.execute(task);
    }

    public synchronized void stopRecord() {
        isStart = false;
        mWaveList.clear();
        resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
        postInvalidate();
    }


    public synchronized void setText(String text) {
        this.text = text;
        postInvalidate();
    }

    public void setUpdateSpeed(int updateSpeed) {
        this.updateSpeed = updateSpeed;
    }
複製程式碼

如果喜歡我的文章,點個贊

如果喜歡我的文章,想與一群資深開發者一起交流學習的話,歡迎加入我的合作群Android Senior Engineer技術交流群。有flutter—效能優化—移動架構—資深UI工程師 —NDK相關專業人員和視訊教學資料
群號:925019412


相關文章