讲真,这不算一个新的课题。早在几个月之前就已经有大神开源了水波浪效果的代码。由于工作关系,一直未能拜读。直至近期公司项目有需要,才决定去接触下该效果。
同时申明一下,在能力范围之内,楼主愿意不惜一切时间代价去造轮子,因为出现bug时,改自己熟悉的代码会很省事。同时也是对技能的巩固和提升。
目前,已经发表的博文中,实现水波纹的效果大致分为两类:
- 使用贝塞尔曲线。
- 使用三角函数曲线。
在物理学中,通常使用三角函数表达式从数学的角度描述波的特征,如:机械波、声波等。因此楼主即使用三角函数实现水波纹效果。
机械波表达式:Y=A*sin(ωt+Φ)
1 效果图
二话不说,先上图,觉得效果好再往下看。
这是一种组合的实现方式:两个水波浪组件叠加,再在水波浪组件上面或下面添加一个矩形的蓝色块。采用组合方式,主要使组件具有更高的扩展性。
布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
<View android:layout_width="match_parent" android:layout_height="@dimen/dp_100_h" android:background="#DD3958AA" />
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content">
<com.flueky.frame.widget.WaveView android:layout_width="match_parent" android:layout_height="@dimen/dp_50_h" app:color="#993958AA" app:fill_mode="top" app:omega="1.2" app:phi="1" app:speed="-5" />
<com.flueky.frame.widget.WaveView android:layout_width="match_parent" android:layout_height="@dimen/dp_50_h" app:color="#AA3958AA" app:fill_mode="top" app:phi="100" app:speed="-5" /> </RelativeLayout>
|
2 原理
分析机械波公式,有三个常量参数:A
、ω
和Φ
。
A
对应组件高度的一半。
ω
对应自定义属性 omega
。
Φ
对应自定义属性 phi
。
通过 ω
和Φ
决定波浪的初始状态,使用speed
属性决定波浪变化速率。
3 实现
3.1 自定义属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <declare-styleable name="wave"> <attr name="color" format="color" /> <attr name="fill_mode"> <enum name="top" value="1" /> <enum name="bottom" value="2" /> </attr> <attr name="speed" format="integer" /> <attr name="phi" format="integer" /> <attr name="omega" format="float"/> </declare-styleable>
|
3.2 代码
不喜欢看又臭又长的代码请跳过,后面有分析。(平生最恨只贴代码不分析的博主)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| public class WaveView extends View { private static final String TAG = "WaveView"; private int width = 0; private int height = 0; private Path shapePath; private int step = 20; private Paint fillPaint;
private double omega; private double phi; private int delta = -2; private boolean fillTop = true; private int waveColor;
public WaveView(Context context) { this(context, null); }
public WaveView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }
public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.wave); initParams(context, typedArray); typedArray.recycle(); }
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.wave); initParams(context, typedArray); typedArray.recycle(); }
private void initParams(Context context, TypedArray typedArray) {
waveColor = typedArray.getColor(R.styleable.wave_color, Color.parseColor("#3958AA")); fillTop = typedArray.getInt(R.styleable.wave_fill_mode, 1) == 1; delta = typedArray.getInt(R.styleable.wave_speed, -2); omega = typedArray.getFloat(R.styleable.wave_omega, 3 * 1.0f / 4); phi = typedArray.getInt(R.styleable.wave_phi, 0) * Math.PI / 180 + Math.PI / 2 * -1; fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); fillPaint.setStrokeWidth(1); fillPaint.setStyle(Paint.Style.FILL); fillPaint.setColor(waveColor); }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); shapePath = new Path(); if (fillTop) shapePath.moveTo(0, 0); else shapePath.moveTo(0, height + 1); for (int x = 0; x <= width; x += step) { double angle = x * 1.0f / width * 2 * Math.PI; double y = height / 2 * Math.sin(angle * omega + phi); shapePath.lineTo(x, (float) y + height / 2); }
if (fillTop) { shapePath.lineTo(width, 0); } else { shapePath.lineTo(width, height + 1); } canvas.drawPath(shapePath, fillPaint);
postInvalidateDelayed(25); addPhi(); }
private void addPhi() { phi += delta * Math.PI / 180;
if (phi > Math.PI * 2) phi -= Math.PI * 2; else if (phi < Math.PI * -2) phi += Math.PI * 2; }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = 0; if (widthMode == MeasureSpec.EXACTLY) { widthSize = MeasureSpec.getSize(widthMeasureSpec); width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { widthSize = width; } int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = 0; if (heightMode == MeasureSpec.EXACTLY) { heightSize = MeasureSpec.getSize(heightMeasureSpec); height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { heightSize = height; } setMeasuredDimension(widthSize, heightSize); }
|
不到120行的代码,看起来是不是很简单的样子。
到这里,需要的小伙伴们,就可以很方便的拿去集成了,一个组件类,一个自定义属性文件和一个布局的使用示例,足以。
下面主要讲解三个模块:初始化参数,测量高度,绘制并刷新波浪。
3.2.1 初始化参数
1 2 3 4 5 6 7 8 9 10 11 12 13
| private void initParams(Context context, TypedArray typedArray) { waveColor = typedArray.getColor(R.styleable.wave_color, Color.parseColor("#3958AA")); fillTop = typedArray.getInt(R.styleable.wave_fill_mode, 1) == 1; delta = typedArray.getInt(R.styleable.wave_speed, -2); omega = typedArray.getFloat(R.styleable.wave_omega, 3 * 1.0f / 4); phi = typedArray.getInt(R.styleable.wave_phi, 0) * Math.PI / 180 + Math.PI / 2 * -1; fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); fillPaint.setStrokeWidth(1); fillPaint.setStyle(Paint.Style.FILL); fillPaint.setColor(waveColor); }
|
3.2.2 测量高度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = 0; if (widthMode == MeasureSpec.EXACTLY) { widthSize = MeasureSpec.getSize(widthMeasureSpec); width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { widthSize = width; } int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = 0; if (heightMode == MeasureSpec.EXACTLY) { heightSize = MeasureSpec.getSize(heightMeasureSpec); height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { heightSize = height; } setMeasuredDimension(widthSize, heightSize); }
|
3.2.3 绘制并刷新波浪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); shapePath = new Path();
if (fillTop) shapePath.moveTo(0, 0); else shapePath.moveTo(0, height + 1); for (int x = 0; x <= width; x += step) { double angle = x * 1.0f / width * 2 * Math.PI; double y = height / 2 * Math.sin(angle * omega + phi); shapePath.lineTo(x, (float) y + height / 2); }
if (fillTop) { shapePath.lineTo(width, 0); } else { shapePath.lineTo(width, height + 1); } canvas.drawPath(shapePath, fillPaint); postInvalidateDelayed(25); addPhi(); }
|
由于,Math.sin
采用弧度计算的方式,为方便开发者使用,关于sin
函数参数的变量,一律使用角度制,因此存在一些难以明白的转换。
3.3 叠加使用
样图中,使用叠加的方式,同时显示两条不一样的波浪。
需要注意修改omega
、phi
、speed
值和正确计算透明度叠加效果。
色值:993958AA
+AA3958AA
= DD3958AA
?
这是ARGB四通道的色值叠加计算,由于RGB色值一致,此处只讨论Alpha通道(透明度)的叠加计算。
记:
透明度 A = 0x99/xFF ≈ 0.60,透明度 B = 0xAA/0xFF ≈ = 0.66;
透明度 C = A+B-AxB = 0.864 x 0xFF ≈ 0xDD;
觉得有用?那打赏一个呗。[去打赏](/donate/)