前言

柱状波形图是一种常见的图形。一个个柱子按顺序排列,形成一个波形图。

柱子的高度由输出数据决定。如果输出的是音频的音量,则可失去一个声波图。

在一些音频软件中,咱们也能够左右拖动声波,来扭转音频的播放进度

本文举例的自定View,实现如下性能:

  • 以柱状模式展现数据的大小
  • 表明图形以后最两头的数据
  • 能够横向拖动进度,进度就是让某个特定的数据居中展现
  • 能够扭转左右两边的柱子色彩
  • 能够调整柱子的宽度
  • 拖动结束后监听以后进度

实现

首先创立类SoundWaveView继承自View

咱们能够先记录给定的宽高,不便前面找到View的两头点

private int viewWid = 1000;     // pxprivate int viewHeight = 100;   // px@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    super.onSizeChanged(w, h, oldw, oldh);    viewWid = w;    viewHeight = h;    // ..}

根本属性

例如柱子的色彩,宽度。能够设置个属性来记录,并凋谢进来可由内部来设置。

private float barWidDp = 1.5f;private float barWidPx = 3f;private float barGapPx = barWidPx / 2;private int barCount = 1;       // 以后宽度能绘制多少个柱子private final Paint paint = new Paint();private int leftColor = Color.GREEN;private int rightColor = Color.LTGRAY;private int middleLineColor = Color.parseColor("#55000000");

设计监听器

拖动结束后,能够将以后进度告诉进来。也能够间接把触摸事件传出去。

public interface OnEvent {    void onMoveEnd(); // 进行拖动了    void onDragTouchEvent(MotionEvent event);}private OnEvent onEventListener;private void tellOnMoveEnd() {    if (onEventListener != null) {        onEventListener.onMoveEnd();    }}

绘制图形

onDraw办法中依据数据绘制图形

本例没有设计背景,间接绘制数据。

图形需要之一是要求某个数据能居中显示,咱们用midIndex来标记这个数据的下标。

比较简单粗犷的实现办法,遍历整个数据列表,计算出每个数据的x坐标。超出范围的不绘制,范畴内的逐个绘制。

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    if (dataList == null || dataList.isEmpty()) {        // draw nothing        drawMiddleLine(canvas);        return;    }    float x0 = viewWid / 2.0f;    if (midIndex > 0) {        x0 = x0 - (barGapPx + barWidPx) * midIndex; // 可能是正数    }    for (int i = 0; i < dataList.size(); i++) {        float d = dataList.get(i);        float x = x0 + (barWidPx + barGapPx) * i;        if (x < 0) {            continue;        }        if (x > viewWid) {            break;        }        if (i <= midIndex) {            paint.setColor(leftColor);        } else {            paint.setColor(rightColor);        }        paint.setStrokeWidth(barWidPx);        float bh = (d / showMaxData) * viewHeight;        bh = Math.max(bh, 4); // 最小也要一点高度 (1)        float bhGap = (viewHeight - bh) / 2f;        canvas.drawLine(x, bhGap, x, viewHeight - bhGap, paint);    }    drawMiddleLine(canvas);}private void drawMiddleLine(Canvas canvas) {    paint.setColor(middleLineColor);    canvas.drawLine(viewWid / 2f, 0, viewWid / 2f, viewHeight, paint);}
  1. 如果数据太小,为了更好看,也要显示一点货色

左右拖动

本例给出的思路是在SoundWaveView中间接获取触摸事件并进行解决。

简略辨别一下模式,分为纯展现和可拖动模式

/*** 单纯播放 展现 无交互*/public static final int MODE_PLAY = 1;/*** 容许左右拖动*/public static final int MODE_CAN_DRAG = 2;

复写onTouchEvent办法,如果是MODE_CAN_DRAG模式,则拦挡触摸事件。判断拖动的横向(x)间隔。

@Overridepublic boolean onTouchEvent(MotionEvent event) {    if (mode == MODE_CAN_DRAG) {        switch (event.getAction()) {            case MotionEvent.ACTION_MOVE:                float dx = (downX - event.getX()); // 不要那么灵活                float movePercent = dx / viewWid;                int dIndex = (int) (movePercent * barCount);                int targetMidIndex = downOldMidIndex + dIndex;                targetMidIndex = Math.max(0, targetMidIndex);                targetMidIndex = Math.min(targetMidIndex, dataList.size() - 1);                setMidIndex(targetMidIndex);                Log.d(TAG, "onTouchEvent-MOVE; dx: " + dx + ", dIndex: " + dIndex + "; targetMidIndex: " + targetMidIndex);                break;            case MotionEvent.ACTION_DOWN:                downX = event.getX();                downOldMidIndex = midIndex;                break;            case MotionEvent.ACTION_CANCEL:            case MotionEvent.ACTION_UP:                downOldMidIndex = midIndex;                tellOnMoveEnd();                break;        }        if (onEventListener != null) {            onEventListener.onDragTouchEvent(event);        }        return true;    }    return super.onTouchEvent(event);}

残缺代码

文件SoundWaveView.java,这个view次要目标是展示声波,取名为「SoundWave」

import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import androidx.annotation.Nullable;import java.util.ArrayList;import java.util.List;/** * @author an.rustfisher.com */public class SoundWaveView extends View {    private static final String TAG = "rustAppSoundWaveView";    /**     * 单纯播放 展现 无交互     */    public static final int MODE_PLAY = 1;    /**     * 容许左右拖动     */    public static final int MODE_CAN_DRAG = 2;    private int mode = MODE_PLAY; // 1 播放    private List<Float> dataList = new ArrayList<>(100);    private float showMaxData = 40f; // 能显示的最大数据    private int midIndex = 0;   // 在两头显示的数据的下标    private float barWidDp = 1.5f;    private float barWidPx = 3f;    private float barGapPx = barWidPx / 2;    private int barCount = 1;       // 以后宽度能绘制多少个柱子    private int viewWid = 1000;     // px    private int viewHeight = 100;   // px    private final Paint paint = new Paint();    private int leftColor = Color.GREEN;    private int rightColor = Color.LTGRAY;    private int middleLineColor = Color.parseColor("#55000000");    private float downX = 0; // getX    private int downOldMidIndex = 0;    public interface OnEvent {        void onMoveEnd(); // 进行拖动了        void onDragTouchEvent(MotionEvent event);    }    private OnEvent onEventListener;    public SoundWaveView(Context context) {        this(context, null);    }    public SoundWaveView(Context context, @Nullable AttributeSet attrs) {        this(context, attrs, 0);    }    public SoundWaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        paint.setColor(Color.BLUE);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        viewWid = w;        viewHeight = h;        calBarPara();        Log.d(TAG, "onSizeChanged: " + w + ", " + h);        Log.d(TAG, "onSizeChanged: barWidPx: " + barWidPx);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        if (dataList == null || dataList.isEmpty()) {            // draw nothing            drawMiddleLine(canvas);            return;        }        float x0 = viewWid / 2.0f;        // 绘制数据        if (midIndex > 0) {            x0 = x0 - (barGapPx + barWidPx) * midIndex; // 可能是正数        }        for (int i = 0; i < dataList.size(); i++) {            float d = dataList.get(i);            float x = x0 + (barWidPx + barGapPx) * i;            if (x < 0) {                continue;            }            if (x > viewWid) {                break;            }            if (i <= midIndex) {                paint.setColor(leftColor);            } else {                paint.setColor(rightColor);            }            paint.setStrokeWidth(barWidPx);            float bh = (d / showMaxData) * viewHeight;            bh = Math.max(bh, 4); // 最小也要一点高度            float bhGap = (viewHeight - bh) / 2f;            canvas.drawLine(x, bhGap, x, viewHeight - bhGap, paint);        }        drawMiddleLine(canvas);    }    private void drawMiddleLine(Canvas canvas) {        paint.setColor(middleLineColor);        canvas.drawLine(viewWid / 2f, 0, viewWid / 2f, viewHeight, paint);    }    public float getMidByPercent() {        return midIndex / (float) (dataList.size() - 1);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        if (mode == MODE_CAN_DRAG) {            switch (event.getAction()) {                case MotionEvent.ACTION_MOVE:                    float dx = (downX - event.getX()); // 不要那么灵活                    float movePercent = dx / viewWid;                    int dIndex = (int) (movePercent * barCount);                    int targetMidIndex = downOldMidIndex + dIndex;                    targetMidIndex = Math.max(0, targetMidIndex);                    targetMidIndex = Math.min(targetMidIndex, dataList.size() - 1);                    setMidIndex(targetMidIndex);                    Log.d(TAG, "onTouchEvent-MOVE; dx: " + dx + ", dIndex: " + dIndex + "; targetMidIndex: " + targetMidIndex);                    break;                case MotionEvent.ACTION_DOWN:                    downX = event.getX();                    downOldMidIndex = midIndex;                    break;                case MotionEvent.ACTION_CANCEL:                case MotionEvent.ACTION_UP:                    downOldMidIndex = midIndex;                    tellOnMoveEnd();                    break;            }            if (onEventListener != null) {                onEventListener.onDragTouchEvent(event);            }            return true;        }        return super.onTouchEvent(event);    }    public void setMode(int mode) {        this.mode = mode;    }    public int getMode() {        return mode;    }    public int getMidIndex() {        return midIndex;    }    public List<Float> getDataList() {        return dataList;    }    public void setOnEventListener(OnEvent onEventListener) {        this.onEventListener = onEventListener;    }    public void clear() {        dataList = new ArrayList<>();        midIndex = 0;        invalidate();    }    private void calBarPara() {        barWidPx = dp2Px(barWidDp);        barGapPx = barWidPx;        barCount = (int) ((viewWid - barGapPx) / (barWidPx + barGapPx));        paint.setStrokeWidth(barWidPx);        Log.d(TAG, "calBarPara: barCount: " + barCount);    }    public void setDataList(List<Float> input) {        dataList = new ArrayList<>(input);        midIndex = 0;        invalidate();    }    public void setMidIndex(int midIndex) {        this.midIndex = midIndex;        invalidate();    }    public void setMidEnd() {        setMidIndex(dataList.size() - 1);    }    // 设置以后播放进度    public void setPlayPercent(float percent) {        midIndex = (int) (percent * (dataList.size() - 1));        if (percent >= 1) {            midIndex = dataList.size() - 1;        }        invalidate();    }    public void setShowMaxData(float showMaxData) {        this.showMaxData = showMaxData;    }    public float getShowMaxData() {        return showMaxData;    }    // 不停地插入数据    public void addDataEnd(float f) {        dataList.add(f);        midIndex = dataList.size() - 1;        invalidate();    }    public void setLeftColor(int leftColor) {        this.leftColor = leftColor;    }    public void setRightColor(int rightColor) {        this.rightColor = rightColor;    }    private float dp2Px(float dp) {        float density = getContext().getResources().getDisplayMetrics().density;        int mark = dp > 0 ? 1 : -1;        return dp * density * mark;    }    private void tellOnMoveEnd() {        if (onEventListener != null) {            onEventListener.onMoveEnd();        }    }}

layout中应用

<com.rustfisher.tutorial2020.customview.soundwave.SoundWaveView    android:id="@+id/sound_wave_view"    android:layout_width="match_parent"    android:layout_height="100dp"    android:layout_marginTop="4dp"    android:background="@android:color/white"    app:layout_constraintTop_toTopOf="parent" />

activity中应用模仿数据

private void setData1() {    List<Float> dataList = new ArrayList<>();    for (int i = 0; i < 1000; i++) {        dataList.add((float) (Math.random() * soundWaveView.getShowMaxData()));    }    soundWaveView.setDataList(dataList);    soundWaveView.setMidIndex(0);    soundWaveView.setOnEventListener(new SoundWaveView.OnEvent() {        @Override        public void onMoveEnd() {            Log.d(TAG, "onMoveEnd: " + soundWaveView.getMidIndex());        }        @Override        public void onDragTouchEvent(MotionEvent event) {            // 在这里能够收到触摸事件        }    });}

运行示例:

咱们也能够扩大一下,假如不应用柱子,也能够把相邻点连接起来,造成折线图的样子。

相干代码在: AndroidTutorial - gitee

扩大浏览

  • 自定义SurfaceView
  • 主动缩放上上限的折线图
  • 监听者模式 - 在Java与Android中的应用