当时我看到三年二班王尼玛发表高仿哔哩哔哩客户端的SearchView这篇文章(没错,搜索布局就是借用这篇文章的,嘿嘿),当时感觉这个动画很酷,于是就迫不及待的试试。 当你看完上面2个gif,是不是忍不住说,你这搞毛啊,谷歌的ViewAnimationUtils不是已经实现了吗,为什么你还去重复的造轮子,这不是浪费表情浪费青春么,大兄弟莫激动,慢慢听我道来: ViewAnimationUtils.createCircularReveal()是安卓5.0才引入的,可以快速实现圆形缩放动画,但是在低版本上使用的话,只要你敢用,我分分钟抛createCircularReveal() not found异常给你看。所以想在低版本上使用,只能靠自己的智慧,自己造轮子了,下面直入主题。
既然轮子造好了,正所谓是骡子是马拉出来溜溜
在你项目的build.gradle添加
compile 'com.cool:expandview:1.0.1'anim_orientation属性说明:
属性属性说明upleft动画从左上方开始扩散upright动画从右上方开始扩散leftbottom动画从左下方开始扩散rightbottom动画从右下方开始扩散center动画从中心开始扩散布局中
<com.cool.expandviewlibrary.ExpandView android:id="@+id/ev_img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10dp" android:visibility="invisible" app:anim_orientation="center" app:anim_duration="500"> <ImageView android:layout_width="350dp" android:layout_height="350dp" android:scaleType="centerCrop" android:src="@mipmap/meizi2" /> </com.cool.expandviewlibrary.ExpandView>这种效果可以通过自定义ViewGroup来实现,在父View中拿到子view的对象,再通过子view获取子view对应的bitmap,有了bitmap,我们就可以搞事情了。首先将viewGroup中的子view设置为invisible,然后偷偷的添加一个真正做动画的view,将这个bitmap交给动画view,然后就可以可以使用xfermode或者bitmapShader了。在这里,我两种方式都试过,都能实现最终效果,但是使用BitmapShader是最为简单的,核心代码只有一行,思路说完了,再总结一下,我们需要自定义2个view,一个ViewGroup,继承FrameLayout,一个做动画的view继承view,接下来直接开始撸码了。
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
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; }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

