code小生,一个专注 Android 领域的技术平台

公众号回复 Android 加入我的安卓技术群

作者:Tyhj
链接:https://www.jianshu.com/p/d2996afeb3e1
声明:本文已获Tyhj授权发表,转发等请联系原作者授权

最近网易云音乐出了一个叫鲸云音效东西,效果怎么样不是很清楚,但是播放界面还带了动效,这个就比较炫酷了,感觉比较有意思,所以也想自己做一个,其中一个我觉得比较好看的效果如下(动图的来源也比较有意思,后面会讲)

具体思路

首先自定义布局是了解的,可能会用到 surfaceView 去绘制,整个动画可以分为四个部分,第一个是旋转的图片,这个好说;第二个是运动并且透明度渐变的三角形,这个画画也简单;第三个是根据音乐变化而变化的一个曲线吧,这个可能比较难,我也没接触过,不过可以试试看,第四个是模糊的背景,这个简单。

具体实现

实现模糊的背景

这个倒是简单,之前也用过一个模糊背景的工具还不错,不过存在一个问题,我是打算自定义一个 surfaceView,给 surfaceView 画一个背景倒是不难,也遇到两个问题

1.怎么将图片以类似自动裁剪居中的方式画上去,这个想想其实简单,取得画布的大小和bitmap的大小,满足一边进行缩放,裁剪掉多余部分就好了

/**
     * 裁剪图片
     *
     * @param rectBitmap
     * @param rectSurface
     */

    public static void centerCrop(Rect rectBitmap, Rect rectSurface) {
        int verticalTimes = rectBitmap.height() / rectSurface.height();
        int horizontalTimes = rectBitmap.width() / rectSurface.width();
        if (verticalTimes > horizontalTimes) {
            rectBitmap.left = 0;
            rectBitmap.right = rectBitmap.right;
            rectBitmap.top = (rectBitmap.height() - (rectSurface.height() * rectBitmap.width() / rectSurface.width())) / 2;
            rectBitmap.bottom = rectBitmap.bottom - rectBitmap.top;
        } else {
            rectBitmap.top = 0;
            rectBitmap.bottom = rectBitmap.bottom;
            rectBitmap.left = (rectBitmap.width() - (rectSurface.width() * rectBitmap.height() / rectSurface.height())) / 2;
            rectBitmap.right = rectBitmap.right - rectBitmap.left;
        }

    }

2.由于我后面画三角形必须得不停地刷新,背景需要重复绘制,感觉有点浪费资源,看了一下局部刷新什么的感觉没什么用,所以就直接先设置为父布局的普通的背景好了,再将 surfaceView 设置为透明

@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) 
{
    setZOrderOnTop(true);
    getHolder().setFormat(PixelFormat.TRANSLUCENT);
}

Android 图片模糊的工具类:
https://www.jianshu.com/p/c676fc51f3ef

实现旋转的图片

这个更简单,为了方便也是直接使用一个 ImageView,通过自带的视图裁剪工具剪裁为圆形,然后通过属性动画来旋转

设置一直旋转的属性动画

objectAnimator = ObjectAnimator.ofFloat(ivShowPic, "rotation"0f360f);
objectAnimator.setDuration(20 * 1000);
objectAnimator.setRepeatMode(ValueAnimator.RESTART);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.setRepeatCount(-1);
objectAnimator.start();

视图裁剪

/**
     * 设置裁剪为圆形
     *
     * @param view
     * @param pading  这个是设置间距是长或宽的几分之一
     */

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public static void setCircleShape(View view, final int pading) {
        view.setClipToOutline(true);
        view.setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                int margin = Math.min(view.getWidth(), view.getHeight()) / pading;
                outline.setOval(margin, margin, view.getWidth() - margin, view.getHeight() - margin);
            }
        });
    }

实现运动的三角形

为了保证性能,这个就得使用 surfaceView 来做了;大体思路就是随机生成一些三角形,三角形速度大小一样,方向随机,从圆中心向外移动,移动过程将透明度减小到零

三角形有速度不过速度大小都一样就先不用管,有速度方向用角度来代替,也好计算运动后的位置,有三个顶点坐标。

所以三角形的初步定义

public class Triangle {

    public Point topPoint1, topPoint2, topPoint3;
    public int moveAngle;

    public Triangle(Point topPoint1, Point topPoint2, Point topPoint3) {
        this.topPoint1 = topPoint1;
        this.topPoint2 = topPoint2;
        this.topPoint3 = topPoint3;
        moveAngle = getMoveAngel();
    }

}

随机生成了三角形

简单的方法,就是先指定一个坐标区域比如x和y从-50到50的这个矩形坐标区域内,随机取点,如果构成三角形就为一个随机三角形,到时候移到中心处只需要x和y坐标各加长宽的一半就好了,方向也是-180度到180度取随机数,便于到时候用斜率计算移动后的位置

画三角形

自定义 surfaceView 的通用写法都一样,随便看一下文章

Android中的 SurfaceView 详解:
https://www.jianshu.com/p/b037249e6d31

我们先清空画布,然后可以随机生成一些三角形,保存所有生成的三角形到一个集合里面,然后设定一个速度,根据每个三角形的方向来计算距离上一次刷新移动到了哪个位置,通过位置计算与中心点的距离来设置透明度,然后画上去

//三角形移动速度
private double moveSpeed = 0.4;
//刷新时间
private static int refreshTime = 20;
//添加两次三角形的间隔
private static int addTriangleInterval = 100;
//每次添加的数量限制
private static int addTriangleOnece = 2;
//总三角形数量
private int allTriangleCount = 100;

mCanvas = mSurfaceHolder.lockCanvas();
mCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
manageTriangle((int) (refreshTime * moveSpeed));
for (Triangle triangle : triangleList) {
    drawTriangle(mCanvas, triangle, mPaintColor);
}
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
Thread.sleep(refreshTime);

具体代码看项目源码,这里注意需要设定几个值来调整动画效果到最佳,做的过程中也有出现一些很魔性的动画,很有意思

然后发现,surfaceView 的动画会出现在 imageView 的上面,虽然我把imageView 的高度调了一下还是没效果,发现是之前设置 surfaceView 透明的时候 setZOrderOnTop(true) 导致的问题;但是如果不设置 surfaceView 又会遮挡背景,的确是没好办法解决

其实可以简单点,判断三角形的移动距离小于 imageView 的时候设置全透明就好了,做出来大概是这样的效果:

视频效果:http://oy5r220jg.bkt.clouddn.com/record__1107012332_1.mp4

其实还是有一点问题的,可以把 Imageview 的旋转在 surfaceView 里面实现,这个应该三角形的出现可以会自然一点,其他解决办法倒是暂时没想到

优化

为了让三角形出现自然一点,可以把 Imageview 的旋转在 surfaceView 里面实现,但是好像不好做,因为还得裁剪图片和控制旋转,相比 imageView 来实现我觉得稍微有点麻烦了;那还可以不设置 setZOrderOnTop(true),这样背景变成了黑色,还需要画一个背景上去;

那么两种方法比较一下,其实模糊化以后的背景质量非常小(图片都模糊了肯定小呀),远远小于要旋转的那张图片的质量,所以绘制surfaceView背景可能比较好;

获取控件的截图

由于我的 surfaceView 不是宽高全屏的,只是中间一部分,而且给surfaceView 设置的背景图片肯定要和整个布局的背景重合,可以先获取背景视图的截图,然后在这里面裁剪出 surfaceView 所在区域

//启用DrawingCache并创建位图
iv_bg.setDrawingCacheEnabled(true);
iv_bg.buildDrawingCache();
//获取bitmap
Bitmap bitmap2 = Bitmap.createBitmap(iv_bg.getDrawingCache());
//裁剪
bitmap2 = Bitmap.createBitmap(bitmap2, 0, jinyunView.getTop(), jinyunView.getWidth(), jinyunView.getHeight());
//bitmap2传给surfaceView
jinyunView.setBitmapBg(bitmap2);
//关闭DrawingCache
iv_bg.setDrawingCacheEnabled(false);

为什么要先获取背景视图的截图,而不直接用那个模糊化的图片呢,因为模糊化的图片尺寸超级小,显示的时候被放大了,而且可能还被裁剪了(背景用的imageView显示的),为保证裁剪后和背景重合还得做很多图象处理,还是直接获取截图来的简单

动态获取颜色

关于三角形的颜色,其实也是要根据背景来设定的

Material Design 鼓励使用动态颜色,新的 Palette 支持库可以提取图片中的一部分颜色来设置你的 UI 的样式来使界面颜色互相搭配以提供一种沉浸式体验。提取出来的调色板(palette)包括突出的和柔和的色调
Vibrant (有活力)
Vibrant dark(有活力 暗色)
Vibrant light(有活力 亮色)
Muted (柔和)
Muted dark(柔和 暗色)
Muted light(柔和 亮色)

就是可以从bitmap中获取几种特殊的颜色,注意获取到的swatche可能为空的

// Palette的部分
Palette palette = Palette.generate(bitmap);
Palette.Swatch swatche = null;
//获取不同风格的颜色,
swatche = palette.getVibrantSwatch();
swatche = palette.getLightVibrantSwatch();
swatche = palette.getDarkVibrantSwatch();
swatche = palette.getMutedSwatch();
//我用这个和网易云接近,其他颜色也都挺漂亮
swatche = palette.getLightMutedSwatch();
swatche = palette.getDarkMutedSwatch();
swatche = palette.getVibrantSwatch();
//获取颜色
int color = swatche.getRgb();

视频效果:http://lc-fgtnb2h8.cn-n1.lcfile.com/7f08b2eea6a4039cf453.mp4
换个颜色:http://lc-fgtnb2h8.cn-n1.lcfile.com/45e70109d2cbc9b7371b.mp4

改变图片的亮度

但是发现一个问题,背景颜色太亮了,我选择palette.getLightMutedSwatch() 是最亮的颜色,还是会被背景干扰,这个设置最上层的布局背景为半透明,发现我 surfaceView 也跟着被半透明覆盖了呀,如果只覆盖背景的话,surfaceView 绘制的背景是从作为背景的ImageVIew 截取的图片,会和背景颜色不一样的,只能从背景 ImageView 入手,还真的有改变亮度的办法,不仅可以改变亮度,还可以改变色相和饱和度

ColorMatrix colorMatrix = new ColorMatrix();
//改变图片亮度
colorMatrix.setScale(0.5f,0.5f,0.5f,1);
ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter(colorMatrix);
iv_bg.setColorFilter(colorFilter);

改变了亮度后对动态获取颜色会有影响,亮色的可能获取不到了,获取颜色应该提前获取

开始画线

仔细看了一下,先画围绕这个圆画很多点,隔一段一个点,然后把点用曲线圈起来就ok了,动的时候就是设置一个上下移动的距离,一个点变成两个,两个点先连线,然后同一侧的点重新连成曲线,感觉是是这样的,先试试

围绕圆画点

这个就是直线和圆的交点问题,从-180度到180度,每间隔一个角度,取斜率计算交点,差不多是这个意思

y = (Math.sin(angle) * circleR);
x = (Math.cos(angle) * circleR);

画出来一看,这是什么情况,根本不均匀,没道理呀,原来是 Math.sin(angle) 和 Math.cos(angle) 里面的值指的是弧度,不是角度,所以转换一下

y = (Math.sin(Math.toRadians(angle)) * circleR);
x = (Math.cos(Math.toRadians(angle)) * circleR);

画贝塞尔曲线

我先用二阶贝塞尔曲线把相邻的点连了起来,中间的点取的是两个点的圆弧中间的点,反正看起来是一个圆

Path path = new Path();
path.moveTo(point.x, point.y);
//画二阶贝塞尔曲线
path.quadTo(bezierPoint.x, bezierPoint.y, next.x, next.y);
canvas.drawPath(path, paint);

原理如下图

二阶贝塞尔曲线

处理点的跳动

到了最后一步,让点分裂成两个分别上下移动后,再次将同一边的连成曲线并将移动后的上下两个点连线,移动距离先取随机数,效果好了再看音频相关东西,这个有点难度,我尝试了很多次,都不是我想要的结果

未标题-3.png

看起来都失败了,感觉这个移动距离不能取随机数,最后一个看起来比较像是手动输入了一组均匀的数据,并且是直接画的直线

获取音频信息

感觉模拟数据不行,还是先看看怎么获取音频信息;获取音频信息比较简单
1.使用MediaPlayer播放传入的音乐,并拿到mediaPlayerId
2.使用Visualizer类拿到拿到MediaPlayer播放中的音频数据(wave/fft)
3.将数据用自定义控件展现出来

使用Visualizer需要录音的动态权限, 如果播放sd卡音频需要STORAGE权限

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

播放音乐

MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.music_wheresilove);
mediaPlayer.setLooping(true);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        mediaPlayer.start();
    }
});

Visualizer回调

Visualizer.OnDataCaptureListener 有2个回调,一个用于显示FFT数据,展示不同频率的振幅,另一个用于显示声音的波形图

private Visualizer.OnDataCaptureListener dataCaptureListener = new Visualizer.OnDataCaptureListener() {
        @Override
        public void onWaveFormDataCapture(Visualizer visualizer, final byte[] waveform, int samplingRate) {
            //到waveform为波形图数据
        }

        @Override
        public void onFftDataCapture(Visualizer visualizer, final byte[] fft, int samplingRate) {
            //FFT数据,展示不同频率的振幅
        }
    };

Visualizer 有两个比较重要的参数
设置可视化数据的数据大小 范围[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()1]
设置可视化数据的采集频率 范围[0~Visualizer.getMaxCaptureRate()]

visualizer = new Visualizer(mediaPlayer.getAudioSessionId());
//采样的最大值
int captureSize = Visualizer.getCaptureSizeRange()[1];
 //采样的频率
int captureRate = Visualizer.getMaxCaptureRate() * 3 / 4;
visualizer.setCaptureSize(captureSize);
visualizer.setDataCaptureListener(dataCaptureListener, captureRate, truetrue);
visualizer.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);
visualizer.setEnabled(true);

有一个很有意思的地方,如果audioSessionId设置为零,就直接获取系统的音频,这个很有意思,连蒙带猜搞出来的

visualizer = new Visualizer(0);

这样纸我们就拿到了两组数据,波形图和频谱图,很显然频谱图是展示不同频率的振幅的,一般情况下只有少部分频率会变动,所以我选择波形图。

拿到的波形图是一个byte数组,里面也是类似每个点的振幅,我们把数组里的数据作为高度画一条线,排成一排正常画出来

 //画音频线
    private void drawAudioLine(Canvas canvas) {
        if (mPoints == null || mPoints.length < mBytes.length * 4) {
            mPoints = new float[mBytes.length * 4];
        }
        for (int i = 1; i < pointSize; i++) {
            if (mBytes[i] < 0) {
                mBytes[i] = 127;
            }
            mPoints[i * 4] = getWidth() * i / pointSize;
            mPoints[i * 4 + 1] = getHeight() / 2;
            mPoints[i * 4 + 2] = getWidth() * i / pointSize;
            mPoints[i * 4 + 3] = 2 + getHeight() / 2 - mBytes[i];
        }
        canvas.drawLines(mPoints, mPaint);
    }

效果是这样纸,用另一个频谱图也差不多,就是变化的区域有点少

这样纸的话,那是不是我把它绕圆一圈,然后在按相反方向绕一圈,同样跳动的两个点连线,然后随便画画曲线是不是就ok啦;做完就发现里面的值太大了,都看不出来是个圆了,那就都减去一点高度什么的,调整一下大小;然后这次就先画一个三次贝塞尔曲线吧,画出来跟跟屎一样,这个曲线是真的难画呀,而且画的慢,看起来不是很流畅;我再次尝试用简单的方法画

折线的顶点时候用圆角,并没有什么乱用

mPaint.setStrokeJoin(Paint.Join.ROUND);

设置path中的连接处有个角度,看起来接近了一些,不过还是差很远

CornerPathEffect cornerPathEffect = new CornerPathEffect(130);
mPaint.setPathEffect(cornerPathEffect);

视频效果:http://lc-fgtnb2h8.cn-n1.lcfile.com/fada1f97f943dd6e944d.mp4

其实可以看出来做法是没有问题的,但是必须先对数据进行处理才能得到想要的效果,但是具体怎么处理这个的确需要不断尝试;如果处理好可以做出更多更好看的效果;

上面的视频效果在github的之前的提交版本里,有兴趣可以找找,现在在不断尝试新的效果,有找到比较好的,会更新上来;有谁搞出炫酷的效果,希望大家不吝赐教

有想法的同学记得告诉我呀

视频转Gif工具实现:https://www.jianshu.com/p/81cb36b610f4

视频的裁剪其实也是上面这个项目的代码,但是暂时没有做功能,会更新
项目地址:https://github.com/tyhjh/Jinyuneffect

分享技术我是认真的

更多相关文章

  1. Android(安卓)OpenGL ES(二)绘制三角形
  2. android 自定义dialog的实现(自定义大小、去除黑色边框等)
  3. Android自定义进度条颜色
  4. Android(安卓)framebuffer 截屏原理
  5. Android(安卓)Q 色彩(颜色)模式解析(一)
  6. Android资源文件Res详解——ShapeDrawable
  7. 【Android】颜色的透明度
  8. Android沉浸式通知栏开源库SystemBarTint源码解析
  9. Android(安卓)SurfaceView 绘图覆盖刷新及脏矩形刷新方法

随机推荐

  1. Java(Android)对文件全文生成MD5摘要
  2. android利用OpenGLES开发动态壁纸用到的G
  3. Android实现计时以及倒计时的几种方法
  4. xml解析(一) XmlPullParser的使用
  5. 【android开发】之【android动态布局方法
  6. 日积月累--android基础知识日记
  7. android 多渠道打包,build.gradle文件配置
  8. android 6.0 创建文件或文件夹失败
  9. Android Thread/Runnable/Handler用法
  10. android startActivityForResult