先上效果图
写在前面:
当时我看到到三年二班王尼玛发表高仿哔哩哔哩客户端的SearchView这篇文章(没错,搜索布局就是借用这篇文章的,嘿嘿),当时感觉这个动画很酷,于是就迫不及待的试试。
当你看完上面2个gif,是不是忍不住说,你这搞毛啊,谷歌的ViewAnimationUtils不是已经实现了吗,为什么你还去重复的造轮子,这不是浪费表情浪费青春么,大兄弟莫激动,慢慢听我道来:
ViewAnimationUtils.createCircularReveal()是安卓5.0才引入的,可以快速实现圆形缩放动画,但是在低版本上使用的话,只要你敢用,我分分钟抛createCircularReveal() not found异常给你看。所以想在低版本上使用,只能靠自己的智慧,自己造轮子了,下面直入主题。
既然轮子造好了,正所谓是骡子是马拉出来溜溜
一、使用:
在你项目的build.gradle添加
compile 'com.cool:expandview:1.0.1'
1.1 自定义属性
属性 | 属性说明 |
---|---|
anim_orientation | 动画开始执行的方向 |
anim_duration | 动画时长 |
centerX | 动画开始圆心x |
centerY | 动画开始圆心y |
anim_orientation属性说明:
属性 | 属性说明 |
---|---|
upleft | 动画从左上方开始扩散 |
upright | 动画从右上方开始扩散 |
leftbottom | 动画从左下方开始扩散 |
rightbottom | 动画从右下方开始扩散 |
center | 动画从中心开始扩散 |
布局中
1.2 使用地方
ExpandView = mImgExpandView = (ExpandView) findViewById(R.id.ev_img);
mImgExpandView.doExpandAnim();//展开动画
mImgExpandView.doPackupAnim();//收缩动画
1.3 使用说明:
- ExpandView只能有一个
子view
- 如果需要作展开动画,请将ExpandView设置为invisible,设置为gone是没有效果的
二、实现思路
这种效果可以通过自定义ViewGroup来实现,在父View中拿到子view的对象,再通过子view获取子view对应的bitmap,有了bitmap,我们就可以搞事情了。首先将viewGroup中的子view设置为invisible,然后偷偷的添加一个真正做动画的view,将这个bitmap交给动画view,然后就可以可以使用xfermode或者bitmapShader了。在这里,我两种方式都试过,都能实现最终效果,但是使用BitmapShader是最为简单的,核心代码只有一行,思路说完了,再总结一下,我们需要自定义2个view,一个ViewGroup,继承FrameLayout,一个做动画的view继承view,接下来直接开始撸码了。
三、动画AnimView
3.1 初始化画笔和圆心坐标
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
mDuration = 500;
mCirclePoint = new PointF(0,0);
}
3.2 开始做展开动画
/**
* 展开动画
*
* @param backgroundAnimView
*/
public void doExpandAnim(View backgroundAnimView) {
createBackgroundBitmap(backgroundAnimView);
if(mBackgroundBitmap == null){
return;
}
startExpandAnim();
}
参数backgroundAnimView是自定义viewGroup传过来的子view,通过这个view来创建相对应的bitmap,看看是如何创建bitmap的。
private void createBackgroundBitmap(View backgroundAnimView) {
width = backgroundAnimView.getWidth();
height = backgroundAnimView.getHeight();
if(width <=0 || height <= 0){
return;
}
mBackgroundBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
backgroundAnimView.draw(new Canvas(mBackgroundBitmap));//创建bitmap
mEndRadius = (float) Math.sqrt(width * width + height * height);//view的对角线,也就是最大的圆心
BitmapShader bitmapShader = new BitmapShader(mBackgroundBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);//mBackgroundBitmap使用bitmapShader
mPaint.setShader(bitmapShader);
}
接下来就是开启动画了
private void startPackupAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(mEndRadius, 0);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentRadius = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(mDuration);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (onPackupAnimEndListener != null) {
onPackupAnimEndListener.onPackupAnimEnd();
}
mBackgroundBitmap.recycle();
mBackgroundBitmap = null;
}
});
valueAnimator.start();
}
onAnimationUpdate不断计算出当前圆心坐标,然后不断的重绘页面,看onDraw()方法
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(mCirclePoint.x, mCirclePoint.y, mCurrentRadius, mPaint);
}
收缩动画和展开动画是一样的,这里就不贴了,下面将目光转向自定义ViewGroup
四、ExpandView
4.1 自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandView);
mAnimOrientation = ta.getInt(R.styleable.ExpandView_anim_orientation,UPLEFT);//动画执行方向
mAnimDuration = ta.getInt(R.styleable.ExpandView_anim_duration,ANIM_DURATION_DEFAULT);//动画执行时间
mCenterX = ta.getDimension(R.styleable.ExpandView_centerX,-1);//动画开始圆心x坐标
mCenterY = ta.getDimension(R.styleable.ExpandView_centerY,-1);//动画开始圆心y坐标
ta.recycle();
4.2 重写generateLayoutParams()方法,让支持子view的margin属性
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MarginLayoutParams(lp);
}
4.3 重写onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();
int expectWidth = 0;
int expectHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0);
int childMeasuredWidth = childView.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
int childMeasuredHeight = childView.getMeasuredHeight();
expectWidth = childMeasuredWidth;
expectHeight = childMeasuredHeight;
}
if(widthMode == MeasureSpec.EXACTLY){
expectWidth = widthSize;
}else {
expectWidth = MeasureSpec.makeMeasureSpec(expectWidth,MeasureSpec.EXACTLY);
}
if(heightMode == MeasureSpec.EXACTLY){
expectHeight = heightSize;
}else {
expectHeight = MeasureSpec.makeMeasureSpec(expectHeight,MeasureSpec.EXACTLY);
}
setMeasuredDimension(expectWidth,expectHeight);
}
首先测量子view的宽高,再设置自身的宽高
4.4 做展开动画doExpandAnim
/**
* 做展开动画
*/
public void doExpandAnim() {
if(isAnimating){//如果还在动画中,什么都不做
return;
}
isAnimating = true;
setVisibility(VISIBLE);
int childCount = getChildCount();
if(childCount >1){
throw new IllegalArgumentException("ExpandView只能有一个子View");
}
if(childCount <=0){
return;
}
setChildViewVisibility(INVISIBLE);//将子view设置不可见
if(animView == null) {
animView = new AnimView(getContext());
}
animView.setOnExpandAnimEndListener(this);
View view = addAnimView(childCount);
animView.doExpandAnim(view);
}
setChildViewVisibility方法
/**
* 设置子view显示或隐藏
* @param visibility 显示或隐藏
*/
private void setChildViewVisibility(int visibility) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if(childView instanceof AnimView){
removeView(childView);
}
childView.setVisibility(visibility);
}
}
addAnimView方法将动画view添加到ExpandAnim中并进行相关初始化
/**
* 获取子view的宽高并添加动画animview
* @param childCount 孩子个数
* @return 返回第一个孩子
*/
@NonNull
private View addAnimView(int childCount) {
View view = getChildAt(0);
mAnimViewWidth = view.getMeasuredWidth();
mAnimViewHight = view.getHeight();
MarginLayoutParams l = (MarginLayoutParams) view.getLayoutParams();
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(mAnimViewWidth,mAnimViewHight);
layoutParams.leftMargin = l.leftMargin;
layoutParams.topMargin = l.topMargin;
layoutParams.rightMargin = l.rightMargin;
layoutParams.bottomMargin = l.bottomMargin;
addView(animView, childCount, layoutParams);
initAnimView(animView);
return view;
}
initAnimView进行animview的初始化
/**
* 设置动画view的一些属性
* @param animView 动画view
*/
private void initAnimView(AnimView animView){
animView.setDuration(mAnimDuration);
animView.setCenterPosition(calculateCirclePoint());
animView.setStartRadius(mStartRadius);
}
/**
* 计算动画起点圆心
* @return 起点圆心
*/
private PointF calculateCirclePoint(){
PointF circlePoint = new PointF();
if(mCenterX != -1 && mCenterY != -1){
circlePoint.set(mCenterX,mCenterY);
return circlePoint;
}
switch (mAnimOrientation) {
case UPLEFT:
mCenterX = mStartRadius;
mCenterY = mStartRadius;
break;
case UPRIGHT:
mCenterX = mAnimViewWidth -mStartRadius;
mCenterY = mStartRadius;
break;
case LEFTBOTTOM:
mCenterX = mStartRadius;
mCenterY = mAnimViewHight - mStartRadius;
break;
case RIGHTBOTTOM:
mCenterX = mAnimViewWidth - mStartRadius;
mCenterY = mAnimViewHight - mStartRadius;
break;
case CENTER:
mCenterX = mAnimViewWidth /2;
mCenterY = mAnimViewHight /2;
break;
}
circlePoint.set(mCenterX,mCenterY);
return circlePoint;
}
4.5做收缩动画
/**
* 做收起动画
*/
public void doPackupAnim() {
if(isAnimating){
return;
}
isAnimating = true;
setVisibility(VISIBLE);
int childCount = getChildCount();
setChildViewVisibility(GONE);
if(childCount <=0){
return;
}
if(animView == null) {
animView = new AnimView(getContext());
}
animView.setOnPackupAnimEndListener(this);
View view = addAnimView(childCount);
animView.doPackupAnim(view);
}
4.6展开动画和收缩动画完成时的监听
@Override
public void onExpandAnimEnd() {
isAnimating = false;
if (animView != null) {
removeView(animView);
}
setChildViewVisibility(VISIBLE);
}
@Override
public void onPackupAnimEnd() {
isAnimating = false;
if (animView != null) {
removeView(animView);
}
setVisibility(INVISIBLE);
}
五,两个view的完整版
ExpandView
/**
* Created by cool on 2017/8/3.
*/
public class ExpandView extends FrameLayout implements AnimView.OnExpandAnimEndListener, AnimView.OnPackupAnimEndListener {
private int mAnimOrientation;
private int mAnimDuration;//动画时长
private float mCenterX;//动画开始圆心x坐标
private float mCenterY;//动画开始圆心y坐标
private final static int UPLEFT = 1;//左上
private final static int UPRIGHT = 2;//右上
private final static int LEFTBOTTOM = 3;//左下
private final static int RIGHTBOTTOM = 4;//右下
private final static int CENTER = 5;//中间
private final static int ANIM_DURATION_DEFAULT = 500;//动画默认时长
private float mStartRadius;//开始执行时的圆半径
private int mAnimViewWidth;
private int mAnimViewHight;
private boolean isAnimating = false;//是否在动画中
private AnimView animView;
public ExpandView(@NonNull Context context) {
this(context, null);
}
public ExpandView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandView);
mAnimOrientation = ta.getInt(R.styleable.ExpandView_anim_orientation,UPLEFT);
mAnimDuration = ta.getInt(R.styleable.ExpandView_anim_duration,ANIM_DURATION_DEFAULT);
mCenterX = ta.getDimension(R.styleable.ExpandView_centerX,-1);
mCenterY = ta.getDimension(R.styleable.ExpandView_centerY,-1);
ta.recycle();
init();
}
private void init() {
mStartRadius = dp2px(5);
}
/**
* 做展开动画
*/
public void doExpandAnim() {
if(isAnimating){
return;
}
isAnimating = true;
setVisibility(VISIBLE);
int childCount = getChildCount();
if(childCount >1){
throw new IllegalArgumentException("ExpandView只能有一个子View");
}
if(childCount <=0){
return;
}
setChildViewVisibility(INVISIBLE);
if(animView == null) {
animView = new AnimView(getContext());
}
animView.setOnExpandAnimEndListener(this);
View view = addAnimView(childCount);
animView.doExpandAnim(view);
}
/**
* 做收起动画
*/
public void doPackupAnim() {
if(isAnimating){
return;
}
isAnimating = true;
setVisibility(VISIBLE);
int childCount = getChildCount();
setChildViewVisibility(GONE);
if(childCount <=0){
return;
}
if(animView == null) {
animView = new AnimView(getContext());
}
animView.setOnPackupAnimEndListener(this);
View view = addAnimView(childCount);
animView.doPackupAnim(view);
}
/**
* 获取子view的宽高并添加动画animview
* @param childCount 孩子个数
* @return 返回第一个孩子
*/
@NonNull
private View addAnimView(int childCount) {
View view = getChildAt(0);
mAnimViewWidth = view.getMeasuredWidth();
mAnimViewHight = view.getHeight();
MarginLayoutParams l = (MarginLayoutParams) view.getLayoutParams();
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(mAnimViewWidth,mAnimViewHight);
layoutParams.leftMargin = l.leftMargin;
layoutParams.topMargin = l.topMargin;
layoutParams.rightMargin = l.rightMargin;
layoutParams.bottomMargin = l.bottomMargin;
addView(animView, childCount, layoutParams);
initAnimView(animView);
return view;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MarginLayoutParams(lp);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();
int expectWidth = 0;
int expectHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams) childView.getLayoutParams();
measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0);
int childMeasuredWidth = childView.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
int childMeasuredHeight = childView.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
expectWidth = childMeasuredWidth;
expectHeight = childMeasuredHeight;
}
if(widthMode == MeasureSpec.EXACTLY){
expectWidth = widthSize;
}else {
expectWidth = MeasureSpec.makeMeasureSpec(expectWidth,MeasureSpec.EXACTLY);
}
if(heightMode == MeasureSpec.EXACTLY){
expectHeight = heightSize;
}else {
expectHeight = MeasureSpec.makeMeasureSpec(expectHeight,MeasureSpec.EXACTLY);
}
setMeasuredDimension(expectWidth,expectHeight);
}
/**
* 设置子view显示或隐藏
* @param visibility 显示或隐藏
*/
private void setChildViewVisibility(int visibility) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if(childView instanceof AnimView){
removeView(childView);
}
childView.setVisibility(visibility);
}
}
/**
* 设置动画view的一些属性
* @param animView 动画view
*/
private void initAnimView(AnimView animView){
animView.setDuration(mAnimDuration);
animView.setCenterPosition(calculateCirclePoint());
animView.setStartRadius(mStartRadius);
}
/**
* 计算动画起点圆心
* @return 起点圆心
*/
private PointF calculateCirclePoint(){
PointF circlePoint = new PointF();
if(mCenterX != -1 && mCenterY != -1){
circlePoint.set(mCenterX,mCenterY);
return circlePoint;
}
switch (mAnimOrientation) {
case UPLEFT:
mCenterX = mStartRadius;
mCenterY = mStartRadius;
break;
case UPRIGHT:
mCenterX = mAnimViewWidth -mStartRadius;
mCenterY = mStartRadius;
break;
case LEFTBOTTOM:
mCenterX = mStartRadius;
mCenterY = mAnimViewHight - mStartRadius;
break;
case RIGHTBOTTOM:
mCenterX = mAnimViewWidth - mStartRadius;
mCenterY = mAnimViewHight - mStartRadius;
break;
case CENTER:
mCenterX = mAnimViewWidth /2;
mCenterY = mAnimViewHight /2;
break;
}
circlePoint.set(mCenterX,mCenterY);
return circlePoint;
}
@Override
public void onExpandAnimEnd() {
isAnimating = false;
if (animView != null) {
removeView(animView);
}
setChildViewVisibility(VISIBLE);
}
@Override
public void onPackupAnimEnd() {
isAnimating = false;
if (animView != null) {
removeView(animView);
}
setVisibility(INVISIBLE);
}
private int dp2px(int dp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
}
AnimView
/**
* Created by cool on 2017/8/3.
*/
public class AnimView extends View {
private Bitmap mBackgroundBitmap;
private Paint mPaint;
private float mEndRadius;
private float mStartRadius;
private PointF mCirclePoint;//封装圆心坐标
private float mCurrentRadius = mStartRadius;
private int width;
private int height;
private long mDuration;
public AnimView(Context context) {
this(context, null);
}
public AnimView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
mDuration = 500;
mCirclePoint = new PointF(0,0);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(mCirclePoint.x, mCirclePoint.y, mCurrentRadius, mPaint);
}
/**
* 展开动画
*
* @param backgroundAnimView
*/
public void doExpandAnim(View backgroundAnimView) {
createBackgroundBitmap(backgroundAnimView);
if(mBackgroundBitmap == null){
return;
}
startExpandAnim();
}
/**
* 收起动画
*/
public void doPackupAnim(View backgroundAnimView) {
createBackgroundBitmap(backgroundAnimView);
if(mBackgroundBitmap == null){
return;
}
startPackupAnim();
}
private void createBackgroundBitmap(View backgroundAnimView) {
width = backgroundAnimView.getWidth();
height = backgroundAnimView.getHeight();
if(width <=0 || height <= 0){
return;
}
mBackgroundBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
backgroundAnimView.draw(new Canvas(mBackgroundBitmap));
mEndRadius = (float) Math.sqrt(width * width + height * height);
BitmapShader bitmapShader = new BitmapShader(mBackgroundBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(bitmapShader);
}
private void startPackupAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(mEndRadius, 0);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentRadius = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.setDuration(mDuration);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (onPackupAnimEndListener != null) {
onPackupAnimEndListener.onPackupAnimEnd();
}
mBackgroundBitmap.recycle();
mBackgroundBitmap = null;
}
});
valueAnimator.start();
}
private void startExpandAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(mStartRadius, mEndRadius);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentRadius = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (onExpandAnimEndListener != null) {
onExpandAnimEndListener.onExpandAnimEnd();
}
mBackgroundBitmap.recycle();
mBackgroundBitmap = null;
}
});
valueAnimator.setDuration(mDuration);
valueAnimator.start();
}
/**
* 设置动画时长
* @param duration 时长
*/
public void setDuration(long duration){
this.mDuration = duration;
}
/**
* 设置圆心坐标
* @param point 圆心坐标
*/
public void setCenterPosition(PointF point){
this.mCirclePoint = point;
}
/**
* 设置开始是圆半径
* @param startRadius 圆半径
*/
public void setStartRadius(float startRadius){
this.mStartRadius = startRadius;
}
private OnExpandAnimEndListener onExpandAnimEndListener;
public void setOnExpandAnimEndListener(OnExpandAnimEndListener listener) {
this.onExpandAnimEndListener = listener;
}
public interface OnExpandAnimEndListener {
void onExpandAnimEnd();
}
private OnPackupAnimEndListener onPackupAnimEndListener;
public void setOnPackupAnimEndListener(OnPackupAnimEndListener listener){
this.onPackupAnimEndListener = listener;
}
public interface OnPackupAnimEndListener{
void onPackupAnimEnd();
}
}
如果有问题,欢迎指出
源码地址:https://github.com/lkkz/ExpandView
欢迎star,issuse