晚上好,现在是凌晨两点半,然后我还在写代码。电脑里播放着《凌晨两点半》,晚上写代码,脑子更清醒,思路更清晰。
今天聊聊属性动画和自定义View搭配使用,前面都讲到自定义View和属性动画,但是一起用的还是不多,刚巧今晚手机提示我更新系统,我看到那个更新的动画还不错,仔细的分析了一下,于是我也决定写一个,不是一模一样的,但是效果和原理是一样的。
先看看图:
这是一张静态的图,这里有三个波浪线,当下载完之后,波浪线会往上活动,一直到消失。
所以难点也是在这个波浪线上。这个波浪线类似于一个水波纹,也有很多的大神写过水波纹的效果,所以当我们知道这个是什么的时候,就比较容易实现这个功能了。
这一次,先看看用到了什么关键的技术。
首先要介绍的,就是高中的时候学过的余弦函数,还记得表达式吗?
y = Asin(wx+b)+h
这个公式里:w影响周期,A影响振幅,h影响y位置,b为初相;
接下来就看看这一次用到了哪些变量:
// y = Asin(wx+b)+h
private static final float STRETCH_FACTOR_A = 50;
private static final int OFFSET_Y = 0;
// 第一条水波移动速度
private static final int TRANSLATE_X_SPEED_ONE = 7;
// 第二条水波移动速度
private static final int TRANSLATE_X_SPEED_TWO = 5;
// 第三条水波移动速度
private static final int TRANSLATE_X_SPEED_Three = 10;
private float mCycleFactorW;
private int mTotalWidth, mTotalHeight;
private float[] mYPositions;
private float[] mResetOneYPositions;
private float[] mResetTwoYPositions;
private float[] mResetThreeYPositions;
private int mXOffsetSpeedOne;
private int mXOffsetSpeedTwo;
private int mXOffsetSpeedThree;
private int mXOneOffset;
private int mXTwoOffset;
private int mXThreeOffset;
private int height = 1200;
private Paint mWavePaint, mWavePaint2,mWavePaint3;
private DrawFilter mDrawFilter;
都是常规的,像画笔、高宽等等。
然后看看初始化的操作,在构造函数里面:
public WaterRipple(Context context, AttributeSet attrs) {
super(context, attrs);
// 将dp转化为px,用于控制不同分辨率上移动速度基本一致
mXOffsetSpeedOne = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_ONE);
mXOffsetSpeedTwo = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_TWO);
mXOffsetSpeedThree = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_Three);
// 初始绘制波纹的画笔
mWavePaint = new Paint();
// 去除画笔锯齿
mWavePaint.setAntiAlias(true);
// 设置风格为实线
mWavePaint.setStyle(Style.FILL);
// 设置画笔颜色
mWavePaint.setColor(Color.parseColor("#ffffff"));
mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
| Paint.FILTER_BITMAP_FLAG);
// 初始绘制波纹的画笔
mWavePaint2 = new Paint();
// 去除画笔锯齿
mWavePaint2.setAntiAlias(true);
// 设置风格为实线
mWavePaint2.setStyle(Style.FILL);
// 设置画笔颜色
mWavePaint2.setColor(Color.parseColor("#eeeeee"));
// 初始绘制波纹的画笔
mWavePaint3 = new Paint();
// 去除画笔锯齿
mWavePaint3.setAntiAlias(true);
// 设置风格为实线
mWavePaint3.setStyle(Style.FILL);
// 设置画笔颜色
mWavePaint3.setColor(Color.parseColor("#ffffff"));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 记录下view的宽高
mTotalWidth = w;
mTotalHeight = h;
// 用于保存原始波纹的y值
mYPositions = new float[mTotalWidth];
// 用于保存波纹一的y值
mResetOneYPositions = new float[mTotalWidth];
// 用于保存波纹二的y值
mResetTwoYPositions = new float[mTotalWidth];
// 用于保存波纹三的y值
mResetThreeYPositions = new float[mTotalWidth];
// 将周期定为view总宽度
mCycleFactorW = (float) (2 * Math.PI / mTotalWidth);
// 根据view总宽度得出所有对应的y值
for (int i = 0; i < mTotalWidth; i++) {
mYPositions[i] = (float) (STRETCH_FACTOR_A
* Math.sin(mCycleFactorW * i) + OFFSET_Y);
}
}
这个初始化倒是不用怎么解释,重点说一下下面的方法:
private void resetPositonY() {
// mXOneOffset代表当前第一条水波纹要移动的距离
int yOneInterval = mYPositions.length - mXOneOffset;
// 使用System.arraycopy方式重新填充第一条波纹的数据
// 第一个参数就是源数组,第二个参数是要复制的源数组中的起始位置,
// 第三个参数是目标数组,第四个参数是要复制到的目标数组的起始位置,第五个参数是要复制的元素的长度
System.arraycopy(mYPositions, mXOneOffset, mResetOneYPositions, 0,
yOneInterval);
System.arraycopy(mYPositions, 0, mResetOneYPositions, yOneInterval,
mXOneOffset);
int yTwoInterval = mYPositions.length - mXTwoOffset;
System.arraycopy(mYPositions, mXTwoOffset, mResetTwoYPositions, 0,
yTwoInterval);
System.arraycopy(mYPositions, 0, mResetTwoYPositions, yTwoInterval,
mXTwoOffset);
int yThreeInterval = mYPositions.length - mXThreeOffset;
System.arraycopy(mYPositions, mXThreeOffset, mResetThreeYPositions, 0,
yThreeInterval);
System.arraycopy(mYPositions, 0, mResetThreeYPositions, yThreeInterval,
mXThreeOffset);
}
这里面用到了一个System.arraycopy的方法,这个在我们平时的时候可能用得比较少,但是确实非常好用,建议大家可以看看这个的具体用法。我这里也是尽量的写满注释,以便更好的理解。
接下来就是最关键的onDraw方法,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 从canvas层面去除绘制时锯齿
canvas.setDrawFilter(mDrawFilter);
resetPositonY();
for (int i = 0; i < mTotalWidth; i++) {
// 绘制第一条水波纹
canvas.drawLine(i, mTotalHeight - mResetOneYPositions[i] - height,
i, mTotalHeight, mWavePaint);
// 绘制第二条水波纹
canvas.drawLine(i, mTotalHeight - mResetTwoYPositions[i] - height
+ 200, i, mTotalHeight, mWavePaint2);
// 绘制第三条水波纹
canvas.drawLine(i, mTotalHeight - mResetThreeYPositions[i] - height
+ 400, i, mTotalHeight, mWavePaint3);
}
// 改变两条波纹的移动点
mXOneOffset += mXOffsetSpeedOne;
mXTwoOffset += mXOffsetSpeedTwo;
mXThreeOffset += mXOffsetSpeedThree;
// 如果已经移动到结尾处,则重新记录
if (mXOneOffset >= mTotalWidth) {
mXOneOffset = 0;
}
if (mXTwoOffset > mTotalWidth) {
mXTwoOffset = 0;
}
if (mXThreeOffset > mTotalWidth) {
mXThreeOffset = 0;
}
}
我这里绘制了三个波浪线,以及处理了一些在绘制中的数据。
最后暴露给外面一个方法,用于动态的调整高度。
public void setHeight(int height) {
this.height = height;
postInvalidate();
}
最重要的View已经绘制出来了,下面看看其他的代码:
首先是布局文件,activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#12ADFF"
tools:context=".MainActivity" >
<com.ltl.waterripple.WaterRipple
android:id="@+id/hehe"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<RelativeLayout
android:id="@+id/relativi"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_margin="5dip"
android:text="设置"
android:textColor="#ffffff"
android:textSize="20sp" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:gravity="center"
android:text="Funtouch OS"
android:textColor="#ffffff"
android:textSize="40sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:gravity="center"
android:text="Power For Android"
android:textColor="#ffffff"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/content_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical"
android:padding="20dip" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="更新版本:vivo rev 1.14.1(81MB)" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginBottom="10dip"
android:layout_marginTop="10dip"
android:background="#eeeeee" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="系统"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更新了部分第三方应用图标" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="优化了系统与一些第三方应用的兼容性" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="优化了应用安装体验" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发现威胁会有弹窗提醒" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dip"
android:background="#eeeeee"
android:padding="1px" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
android:clickable="true"
android:gravity="center"
android:onClick="onClick"
android:padding="5dip"
android:text="下载并安装"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/progress_linear"
android:layout_width="match_parent"
android:layout_height="200dip"
android:layout_alignParentBottom="true"
android:gravity="center|bottom"
android:orientation="vertical"
android:padding="20dip"
android:visibility="gone" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp"
android:text="当前版本:2.354785" />
<TextView
android:id="@+id/text_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="状态" />
<ProgressBar
android:id="@+id/progressBar1"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
这一次的布局写的比较多,也是用到了多层的嵌套。所以在阅读的时候要很注意细节。
接下来是MainActivity.java:
public class MainActivity extends Activity {
private int height2 = 1200;
private int size = 0;
private ProgressBar bar;
private int height;
private WaterRipple ripple;
private TextView text_state;
private LinearLayout layout1, layout2;
private RelativeLayout layout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
ripple = (WaterRipple) findViewById(R.id.hehe);
bar = (ProgressBar) findViewById(R.id.progressBar1);
layout1 = (LinearLayout) findViewById(R.id.content_linear);
layout2 = (LinearLayout) findViewById(R.id.progress_linear);
text_state = (TextView) findViewById(R.id.text_state);
layout = (RelativeLayout) findViewById(R.id.relativi);
WindowManager wm = this.getWindowManager();
height = wm.getDefaultDisplay().getHeight();
height2 = (int)(height*0.7);
ripple.setHeight(height2);
bar.setMax(81);
}
Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1:
if (size < 81) {
bar.setProgress(size);
text_state.setText("下载进度:"
+ (float) (((float) (size) / 81) * 100) + "%");
size++;
} else {
startAnim();
layout2.setVisibility(View.GONE);
}
break;
case 2:
ripple.setHeight(height2);
ViewGroup.LayoutParams params = layout.getLayoutParams();
params.height = height - height2;
layout.setLayoutParams(params);
height2++;
break;
case 3:
ObjectAnimator animator = ObjectAnimator.ofFloat(ripple,
"alpha", 1f, 0f,0.5f);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(layout,
"alpha", 1f, 0f,0.5f);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animator, animator2);
animatorSet.setDuration(300);
animatorSet.start();
break;
default:
break;
}
};
};
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
while (size < 81) {
SystemClock.sleep(100);
handler.sendEmptyMessage(1);
}
}
}).start();
layout1.setVisibility(View.GONE);
layout2.setVisibility(View.VISIBLE);
}
public void startAnim() {
new Thread(new Runnable() {
public void run() {
while (height2 < height) {
SystemClock.sleep(100);
handler.sendEmptyMessage(2);
}
handler.sendEmptyMessage(3);
}
}).start();
}
}
这里我有两个线程,主要是控制下载的进度展示和水波纹效果。具体的参数,使用者可以根据自己的需要,再作调整。
这里面也用了一个工具类DensityUtil.java:
public class DensityUtil {
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
还有那个控件WaterRipple.java:
public class WaterRipple extends View {
// y = Asin(wx+b)+h
private static final float STRETCH_FACTOR_A = 50;
private static final int OFFSET_Y = 0;
// 第一条水波移动速度
private static final int TRANSLATE_X_SPEED_ONE = 7;
// 第二条水波移动速度
private static final int TRANSLATE_X_SPEED_TWO = 5;
// 第三条水波移动速度
private static final int TRANSLATE_X_SPEED_Three = 10;
private float mCycleFactorW;
private int mTotalWidth, mTotalHeight;
private float[] mYPositions;
private float[] mResetOneYPositions;
private float[] mResetTwoYPositions;
private float[] mResetThreeYPositions;
private int mXOffsetSpeedOne;
private int mXOffsetSpeedTwo;
private int mXOffsetSpeedThree;
private int mXOneOffset;
private int mXTwoOffset;
private int mXThreeOffset;
private int height = 1200;
private Paint mWavePaint, mWavePaint2,mWavePaint3;
private DrawFilter mDrawFilter;
public WaterRipple(Context context, AttributeSet attrs) {
super(context, attrs);
// 将dp转化为px,用于控制不同分辨率上移动速度基本一致
mXOffsetSpeedOne = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_ONE);
mXOffsetSpeedTwo = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_TWO);
mXOffsetSpeedThree = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_Three);
// 初始绘制波纹的画笔
mWavePaint = new Paint();
// 去除画笔锯齿
mWavePaint.setAntiAlias(true);
// 设置风格为实线
mWavePaint.setStyle(Style.FILL);
// 设置画笔颜色
mWavePaint.setColor(Color.parseColor("#ffffff"));
mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
| Paint.FILTER_BITMAP_FLAG);
// 初始绘制波纹的画笔
mWavePaint2 = new Paint();
// 去除画笔锯齿
mWavePaint2.setAntiAlias(true);
// 设置风格为实线
mWavePaint2.setStyle(Style.FILL);
// 设置画笔颜色
mWavePaint2.setColor(Color.parseColor("#eeeeee"));
// 初始绘制波纹的画笔
mWavePaint3 = new Paint();
// 去除画笔锯齿
mWavePaint3.setAntiAlias(true);
// 设置风格为实线
mWavePaint3.setStyle(Style.FILL);
// 设置画笔颜色
mWavePaint3.setColor(Color.parseColor("#ffffff"));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 从canvas层面去除绘制时锯齿
canvas.setDrawFilter(mDrawFilter);
resetPositonY();
for (int i = 0; i < mTotalWidth; i++) {
// 减400只是为了控制波纹绘制的y的在屏幕的位置,大家可以改成一个变量,然后动态改变这个变量,从而形成波纹上升下降效果
// 绘制第一条水波纹
canvas.drawLine(i, mTotalHeight - mResetOneYPositions[i] - height,
i, mTotalHeight, mWavePaint);
// 绘制第二条水波纹
canvas.drawLine(i, mTotalHeight - mResetTwoYPositions[i] - height
+ 200, i, mTotalHeight, mWavePaint2);
// 绘制第三条水波纹
canvas.drawLine(i, mTotalHeight - mResetThreeYPositions[i] - height
+ 400, i, mTotalHeight, mWavePaint3);
}
// 改变两条波纹的移动点
mXOneOffset += mXOffsetSpeedOne;
mXTwoOffset += mXOffsetSpeedTwo;
mXThreeOffset += mXOffsetSpeedThree;
// 如果已经移动到结尾处,则重头记录
if (mXOneOffset >= mTotalWidth) {
mXOneOffset = 0;
}
if (mXTwoOffset > mTotalWidth) {
mXTwoOffset = 0;
}
if (mXThreeOffset > mTotalWidth) {
mXThreeOffset = 0;
}
// 引发view重绘,一般可以考虑延迟20-30ms重绘,空出时间片
}
private void resetPositonY() {
// mXOneOffset代表当前第一条水波纹要移动的距离
int yOneInterval = mYPositions.length - mXOneOffset;
// 使用System.arraycopy方式重新填充第一条波纹的数据
// 第一个参数就是源数组,第二个参数是要复制的源数组中的起始位置,
// 第三个参数是目标数组,第四个参数是要复制到的目标数组的起始位置,第五个参数是要复制的元素的长度
System.arraycopy(mYPositions, mXOneOffset, mResetOneYPositions, 0,
yOneInterval);
System.arraycopy(mYPositions, 0, mResetOneYPositions, yOneInterval,
mXOneOffset);
int yTwoInterval = mYPositions.length - mXTwoOffset;
System.arraycopy(mYPositions, mXTwoOffset, mResetTwoYPositions, 0,
yTwoInterval);
System.arraycopy(mYPositions, 0, mResetTwoYPositions, yTwoInterval,
mXTwoOffset);
int yThreeInterval = mYPositions.length - mXThreeOffset;
System.arraycopy(mYPositions, mXThreeOffset, mResetThreeYPositions, 0,
yThreeInterval);
System.arraycopy(mYPositions, 0, mResetThreeYPositions, yThreeInterval,
mXThreeOffset);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 记录下view的宽高
mTotalWidth = w;
mTotalHeight = h;
// 用于保存原始波纹的y值
mYPositions = new float[mTotalWidth];
// 用于保存波纹一的y值
mResetOneYPositions = new float[mTotalWidth];
// 用于保存波纹二的y值
mResetTwoYPositions = new float[mTotalWidth];
// 用于保存波纹三的y值
mResetThreeYPositions = new float[mTotalWidth];
// 将周期定为view总宽度
mCycleFactorW = (float) (2 * Math.PI / mTotalWidth);
// 根据view总宽度得出所有对应的y值
for (int i = 0; i < mTotalWidth; i++) {
mYPositions[i] = (float) (STRETCH_FACTOR_A
* Math.sin(mCycleFactorW * i) + OFFSET_Y);
}
}
public void setHeight(int height) {
this.height = height;
postInvalidate();
}
}
以上就是全部内容,有个bug,就是在转换的时候部分手机会出现卡顿,后面我加上插值器调整一下,先睡了。如果代码有什么不妥的地方欢迎指出。