前段时间写了一篇项目总结的文章,总结了项目中使用的弧形View 和弧形ViewPager 效果,采用的是自定义View 的方法,然后绘制弧形采用的是二阶贝塞尔曲线,具体的思路和详情请看文章 ,最后效果如下:
虽然效果还不错,但是有瑕疵,有两个明显的缺陷:
-
底部的圆弧不是正圆弧:如上图所示,弧形有点歪,特别是在小屏幕手机上表现尤为明显,因为是用二阶贝塞尔曲线绘制的圆弧,不管怎么调整控制点,都不会是一个正圆弧,如下图:
-
圆弧不能设置图片背景:前面的这个版本,弧形背景只能设置颜色,不能设置背景图
1. 升级版ArcView实现思路
既然有了上面说的2个缺点,我们就要想办法解决它,2个问题我们逐个分析一下:
1. 圆弧问题:
版本1的弧形使用二阶贝塞尔曲线绘制,既然这种方式不能绘制一个正圆弧,那么我们不妨换个思路,哪些图形有正圆弧?首先就想到了圆,我们可以绘制一个很大的圆,然后用手机的屏幕去截取,重叠的部分就是我们想要View了,画了一个草图,看得比较直观:
如上图所示,圆形和屏幕的重叠区域就是我们的View区域,圆形重叠之外的区域在屏幕外。这样截取出来的弧形肯定是正圆弧。
2 . 弧形View设置图片背景
我们采用的是自定义View,显示图片还是很简单的,canvas
的drawBitmap
就能实现,但是有一个点,图片要显示成我们定义的弧形,这就需要用到 PorterDuffXfermode
,PorterDuff.Mode
,关于PorterDuffXfermode这里不过多的讲,网上讲它的博客很多,看一下这张经典的图就行白了:
具体实现:先绘制圆,再绘制图片,设置 PorterDuffXfermode
为 PorterDuff.Mode.SRC_IN
就ok了。
2. 具体实现
前面说了思路,那么代码就很简单了,看一下实现的代码:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mHeight = getHeight(); int width = getWidth(); mWidth = width; // 半径 mRadius = width * 2; // 矩形 mRect.left = 0; mRect.top = 0; mRect.right = width; mRect.bottom = mHeight; // 圆心坐标 mCircleCenter.x = width / 2; mCircleCenter.y = mHeight - width * 2; // 绘制渐变色 mLinearGradient = new LinearGradient(width / 2, 0, width / 2, mHeight, mStartColor, mEndColor, Shader.TileMode.MIRROR); }复制代码
解释:圆的半径为屏幕宽度2倍,矩形的高度就是整个自定义View的高度,圆心坐标的y 为 mHeight - mRadius 。
@Override protected void onDraw(Canvas canvas) { int canvasWidth = canvas.getWidth(); int canvasHeight = canvas.getHeight(); int layerId = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG); canvas.drawCircle(mCircleCenter.x, mCircleCenter.y, mRadius, mPaint); //设置PorterDuffXfermode mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); // 通过变量mIsShowImage 来控制是显示图片还是颜色 if (mIsShowImage) { if (mBitmap != null) { canvas.drawBitmap(mBitmap, null, mRect, mPaint); } } else { mPaint.setShader(mLinearGradient);//绘制渐变色 canvas.drawRect(mRect, mPaint); } mPaint.setXfermode(null); canvas.restoreToCount(layerId); }复制代码
就是这么简单,最后效果如下:
效果是不是好了很多?
3. 完整源码
PerfectArcView.java
public class PerfectArcView extends View implements Target { private Paint mPaint; private Bitmap mBitmap; private int mHeight; private int mWidth; private RectF mRect = new RectF(); private Point mCircleCenter; private float mRadius; private int mStartColor; private int mEndColor; private LinearGradient mLinearGradient; /** * 显示图片还是显示色值 */ private boolean mIsShowImage = true; public PerfectArcView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); readAttr(attrs); init(); } private void init() { setLayerType(View.LAYER_TYPE_SOFTWARE, null); mPaint = new Paint(); mPaint.setColor(Color.WHITE); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); // mBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.splash); mCircleCenter = new Point(); } private void readAttr(AttributeSet set) { TypedArray typedArray = getContext().obtainStyledAttributes(set, R.styleable.PerfectArcView); mStartColor = typedArray.getColor(R.styleable.PerfectArcView_p_arc_startColor, Color.parseColor("#FF3A80")); mEndColor = typedArray.getColor(R.styleable.PerfectArcView_p_arc_endColor, Color.parseColor("#FF3745")); mIsShowImage = typedArray.getBoolean(R.styleable.PerfectArcView_p_arc_showImage, false); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mHeight = getHeight(); int width = getWidth(); mWidth = width; // 半径 mRadius = width * 2; // 矩形 mRect.left = 0; mRect.top = 0; mRect.right = width; mRect.bottom = mHeight; // 圆心坐标 mCircleCenter.x = width / 2; mCircleCenter.y = mHeight - width * 2; mLinearGradient = new LinearGradient(width / 2, 0, width / 2, mHeight, mStartColor, mEndColor, Shader.TileMode.MIRROR); } /** * 加载网络图片 * * @param url */ public void setImageUrl(String url) { Picasso.with(getContext()).load(url).into(this); } /** * @param startColor * @param endColor */ public void setColor(@ColorInt int startColor, @ColorInt int endColor) { mStartColor = startColor; mEndColor = endColor; mIsShowImage = false; mLinearGradient = new LinearGradient(mWidth / 2, 0, mWidth / 2, mHeight, mStartColor, mEndColor, Shader.TileMode.MIRROR); invalidate(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onDraw(Canvas canvas) { int canvasWidth = canvas.getWidth(); int canvasHeight = canvas.getHeight(); int layerId = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG); canvas.drawCircle(mCircleCenter.x, mCircleCenter.y, mRadius, mPaint); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); if (mIsShowImage) { if (mBitmap != null) { canvas.drawBitmap(mBitmap, null, mRect, mPaint); } } else { mPaint.setShader(mLinearGradient);//绘制渐变色 canvas.drawRect(mRect, mPaint); } mPaint.setXfermode(null); canvas.restoreToCount(layerId); } @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { Log.e("TAG", "onBitmapLoaded...."); mBitmap = bitmap; invalidate(); } @Override public void onBitmapFailed(Drawable errorDrawable) { Log.e("TAG", "onBitmapFailed...."); } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { Log.e("TAG", "onPrepareLoad...."); }}复制代码
4. 最后
条条大路通罗马,本文讲了弧形View的另一种实现思路,当然了,可能还有很多种实现方法,上一篇文章的留言区里,有人同学提到可以在矩形区域的地步覆盖一个白色的弧形图片,这个白色的可以找UI设计师切图,这种应该也是可以实现效果的,但是扩展性不是很强,如果项目中有多个地方用到,还是挺麻烦的。如果你还有其他方法,欢迎交流。 源码访问Github:https://github.com/pinguo-zhouwei/AndroidTrainingSimples