转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41967509,本文出自:【张鸿洋的博客】 1、概述 记得初学那会写过一篇博客Android 完美实现图片圆角和圆形(对实现进行分析),主要是个自定View加上使用Xfermode实现的。其实实现圆角图片的方法应该很多,常见的就是利用Xfermode,Shader。本篇博客会直接继承直接继承ImageView,使用BitmapShader实现圆角的绘制,大家如果耐着性子看完,我估计什么形状都能绘制出来。 2、效果图 这是圆角的一个演示图~~这个没什么说的,直接设置的圆角的大小就行; 这是圆形的显示图,这里需要注意下,因为设置的图片可能是长方形,例如上图:有两个长方形,一个宽比较大,一个高比较大; 那么我们希望显示成圆形,我们可能就要对其进行放大或者缩小(因为图片的宽可能不满足设置的边长,而高超出,此时我们就需要放大其宽度)。 这个一张图,中间是正常尺寸;上下分别为特大特小,主要可以当尺寸大于或者小于设置尺寸,我们需要对其放大或者缩小; 圆角时如果图片与view的宽高不一致,也需要进行放大缩小,这里就不截图了,代码里面看吧。 3、浅谈BitmapShader BitmapShader是Shader的子类,可以通过Paint.setShader(Shader shader)进行设置、 这里我们只关注BitmapShader,构造方法: mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP); 参数1:bitmap 参数2,参数3:TileMode; TileMode的取值有三种: CLAMP 拉伸 REPEAT 重复 MIRROR 镜像 如果大家给电脑屏幕设置屏保的时候,如果图片太小,可以选择重复、拉伸、镜像; 重复:就是横向、纵向不断重复这个bitmap 镜像:横向不断翻转重复,纵向不断翻转重复; 拉伸:这个和电脑屏保的模式应该有些不同,这个拉伸的是图片最后的那一个像素;横向的最后一个横行像素,不断的重复,纵项的那一列像素,不断的重复; 现在大概明白了,BitmapShader通过设置给mPaint,然后用这个mPaint绘图时,就会根据你设置的TileMode,对绘制区域进行着色。 这里需要注意一点:就是BitmapShader是从你的画布的左上角开始绘制的,不在view的右下角绘制个正方形,它不会在你正方形的左上角开始。 好了,到此,我相信大家对BitmapShader有了一定的了解了;当然了,如果你希望对Shader充分的了解,请参考爱歌的神作: 自定义控件其实很简单1/3 。 对于我们的圆角,以及圆形,我们设置的模式都是CLAMP ,但是你会不会会有一个疑问: view的宽或者高大于我们的bitmap宽或者高岂不是会拉伸? 嗯,我们会为BitmapShader设置一个matrix,去适当的放大或者缩小图片,不会让“ view的宽或者高大于我们的bitmap宽或者高 ”此条件成立的。 到此我们的原理基本介绍完毕了,拿到drawable转化为bitmap,然后直接初始化BitmapShader,画笔设置Shader,最后在onDraw里面进行画圆就行了。 4、BitmapShader实战 首先就来看看利用BitmapShader实现的圆形或者圆角。 我们这里直接继承ImageView,这样大家设置图片的代码会比较熟悉;但是我们需要支持两种模式,那么就需要自定义属性了: 1、自定义属性 values/attr.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="borderRadius" format="dimension" /> <attr name="type"> <enum name="circle" value="0" /> <enum name="round" value="1" /> </attr> <declare-styleable name="RoundImageView"> <attr name="borderRadius" /> <attr name="type" /> </declare-styleable> </resources>
我们定义了一个枚举和一个圆角的大小borderRadius。
2、获取自定义属性
1 public class RoundImageView extends ImageView 2 { 3 4 /** 5 * 图片的类型,圆形or圆角 6 */ 7 private int type; 8 private static final int TYPE_CIRCLE = 0; 9 private static final int TYPE_ROUND = 1; 10 11 /** 12 * 圆角大小的默认值 13 */ 14 private static final int BODER_RADIUS_DEFAULT = 10; 15 /** 16 * 圆角的大小 17 */ 18 private int mBorderRadius; 19 20 /** 21 * 绘图的Paint 22 */ 23 private Paint mBitmapPaint; 24 /** 25 * 圆角的半径 26 */ 27 private int mRadius; 28 /** 29 * 3x3 矩阵,主要用于缩小放大 30 */ 31 private Matrix mMatrix; 32 /** 33 * 渲染图像,使用图像为绘制图形着色 34 */ 35 private BitmapShader mBitmapShader; 36 /** 37 * view的宽度 38 */ 39 private int mWidth; 40 private RectF mRoundRect; 41 42 public RoundImageView(Context context, AttributeSet attrs) 43 { 44 super(context, attrs); 45 mMatrix = new Matrix(); 46 mBitmapPaint = new Paint(); 47 mBitmapPaint.setAntiAlias(true); 48 49 TypedArray a = context.obtainStyledAttributes(attrs, 50 R.styleable.RoundImageView); 51 52 mBorderRadius = a.getDimensionPixelSize( 53 R.styleable.RoundImageView_borderRadius, (int) TypedValue 54 .applyDimension(TypedValue.COMPLEX_UNIT_DIP, 55 BODER_RADIUS_DEFAULT, getResources() 56 .getDisplayMetrics()));// 默认为10dp 57 type = a.getInt(R.styleable.RoundImageView_type, TYPE_CIRCLE);// 默认为Circle 58 59 a.recycle(); 60 }
可以看到我们的一些成员变量,基本都加了注释;然后在构造方法中获取了我们的自定义属性,以及部分变量的初始化。
3、onMeasure
1 @Override 2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 3 { 4 Log.e("TAG", "onMeasure"); 5 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 6 7 /** 8 * 如果类型是圆形,则强制改变view的宽高一致,以小值为准 9 */ 10 if (type == TYPE_CIRCLE) 11 { 12 mWidth = Math.min(getMeasuredWidth(), getMeasuredHeight()); 13 mRadius = mWidth / 2; 14 setMeasuredDimension(mWidth, mWidth); 15 } 16 17 }
我们复写了onMeasure方法,主要用于当设置类型为圆形时,我们强制让view的宽和高一致。
接下来只剩下设置BitmapShader和绘制了
4、设置BitmapShader
1 /** 2 * 初始化BitmapShader 3 */ 4 private void setUpShader() 5 { 6 Drawable drawable = getDrawable(); 7 if (drawable == null) 8 { 9 return; 10 } 11 12 Bitmap bmp = drawableToBitamp(drawable); 13 // 将bmp作为着色器,就是在指定区域内绘制bmp 14 mBitmapShader = new BitmapShader(bmp, TileMode.CLAMP, TileMode.CLAMP); 15 float scale = 1.0f; 16 if (type == TYPE_CIRCLE) 17 { 18 // 拿到bitmap宽或高的小值 19 int bSize = Math.min(bmp.getWidth(), bmp.getHeight()); 20 scale = mWidth * 1.0f / bSize; 21 22 } else if (type == TYPE_ROUND) 23 { 24 // 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值; 25 scale = Math.max(getWidth() * 1.0f / bmp.getWidth(), getHeight() 26 * 1.0f / bmp.getHeight()); 27 } 28 // shader的变换矩阵,我们这里主要用于放大或者缩小 29 mMatrix.setScale(scale, scale); 30 // 设置变换矩阵 31 mBitmapShader.setLocalMatrix(mMatrix); 32 // 设置shader 33 mBitmapPaint.setShader(mBitmapShader); 34 }
在setUpShader中,首先对drawable转化为我们的bitmap;
然后初始化mBitmapShader = new BitmapShader(bmp, TileMode.CLAMP, TileMode.CLAMP);
接下来,根据类型以及bitmap和view的宽高,计算scale;
关于scale的计算:
圆形时:取bitmap的宽或者高的小值作为基准,如果采用大值,缩放后肯定不能填满我们的圆形区域。然后,view的mWidth/bSize ; 得到的就是scale。
圆角时:因为设计到宽/高比例,我们分别getWidth() * 1.0f / bmp.getWidth() 和 getHeight() * 1.0f / bmp.getHeight() ;最终取大值,因为我们要让最终缩放完成的图片一定要大于我们的view的区域,有点类似centerCrop;
比如:view的宽高为10*20;图片的宽高为5*100 ; 最终我们应该按照宽的比例放大,而不是按照高的比例缩小;因为我们需要让缩放后的图片,自定大于我们的view宽高,并保证原图比例。
有了scale,就可以设置给我们的matrix;
然后使用mBitmapShader.setLocalMatrix(mMatrix);
最后将bitmapShader设置给paint。
关于drawable转bitmap的代码:
1 /** 2 * drawable转bitmap 3 * 4 * @param drawable 5 * @return 6 */ 7 private Bitmap drawableToBitamp(Drawable drawable) 8 { 9 if (drawable instanceof BitmapDrawable) 10 { 11 BitmapDrawable bd = (BitmapDrawable) drawable; 12 return bd.getBitmap(); 13 } 14 int w = drawable.getIntrinsicWidth(); 15 int h = drawable.getIntrinsicHeight(); 16 Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 17 Canvas canvas = new Canvas(bitmap); 18 drawable.setBounds(0, 0, w, h); 19 drawable.draw(canvas); 20 return bitmap; 21 }
最后我们会在onDraw里面调用setUpShader(),然后进行绘制。
5、绘制
到此,就剩下最后一步绘制了,因为我们的范围,以及缩放都完成了,所以真的只剩下绘制了。
1 @Override 2 protected void onDraw(Canvas canvas) 3 { 4 if (getDrawable() == null) 5 { 6 return; 7 } 8 setUpShader(); 9 10 if (type == TYPE_ROUND) 11 { 12 canvas.drawRoundRect(mRoundRect, mBorderRadius, mBorderRadius, 13 mBitmapPaint); 14 } else 15 { 16 canvas.drawCircle(mRadius, mRadius, mRadius, mBitmapPaint); 17 // drawSomeThing(canvas); 18 } 19 } 20 21 @Override 22 protected void onSizeChanged(int w, int h, int oldw, int oldh) 23 { 24 super.onSizeChanged(w, h, oldw, oldh); 25 // 圆角图片的范围 26 if (type == TYPE_ROUND) 27 mRoundRect = new RectF(0, 0, getWidth(), getHeight()); 28 }
绘制就很简单了,画个圆,圆角矩形什么的。圆角矩形的限定范围mRoundRect在onSizeChanged里面进行了初始化。
5、状态的存储与恢复
当然了,如果内存不足,而恰好我们的Activity置于后台,不幸被重启,或者用户旋转屏幕造成Activity重启,我们的View应该也能尽可能的去保存自己的属性。
状态保存什么用处呢?比如,现在一个的圆角大小是10dp,用户点击后变成50dp;当用户旋转以后,或者长时间置于后台以后,返回我们的Activity应该还是50dp;
我们简单的存储一下,当前的type以及mBorderRadius
1 private static final String STATE_INSTANCE = "state_instance"; 2 private static final String STATE_TYPE = "state_type"; 3 private static final String STATE_BORDER_RADIUS = "state_border_radius"; 4 5 @Override 6 protected Parcelable onSaveInstanceState() 7 { 8 Bundle bundle = new Bundle(); 9 bundle.putParcelable(STATE_INSTANCE, super.onSaveInstanceState()); 10 bundle.putInt(STATE_TYPE, type); 11 bundle.putInt(STATE_BORDER_RADIUS, mBorderRadius); 12 return bundle; 13 } 14 15 @Override 16 protected void onRestoreInstanceState(Parcelable state) 17 { 18 if (state instanceof Bundle) 19 { 20 Bundle bundle = (Bundle) state; 21 super.onRestoreInstanceState(((Bundle) state) 22 .getParcelable(STATE_INSTANCE)); 23 this.type = bundle.getInt(STATE_TYPE); 24 this.mBorderRadius = bundle.getInt(STATE_BORDER_RADIUS); 25 } else 26 { 27 super.onRestoreInstanceState(state); 28 } 29 30 }
代码比较简单。我们文章中的demo中,第一个,第四个是可以点击的,点击后会发生变化,你可以点击后,然后旋转屏幕进行测试。
同时我们也对外公布了两个方法,用于动态修改圆角大小和type
1 public void setBorderRadius(int borderRadius) 2 { 3 int pxVal = dp2px(borderRadius); 4 if (this.mBorderRadius != pxVal) 5 { 6 this.mBorderRadius = pxVal; 7 invalidate(); 8 } 9 } 10 11 public void setType(int type) 12 { 13 if (this.type != type) 14 { 15 this.type = type; 16 if (this.type != TYPE_ROUND && this.type != TYPE_CIRCLE) 17 { 18 this.type = TYPE_CIRCLE; 19 } 20 requestLayout(); 21 } 22 23 } 24 25 public int dp2px(int dpVal) 26 { 27 return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28 dpVal, getResources().getDisplayMetrics()); 29 }
最后贴一下我们的布局文件和MainActivity。
6、调用
布局文件:
1 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 xmlns:zhy="http://schemas.android.com/apk/res/com.zhy.variousshapeimageview" 4 android:layout_width="match_parent" 5 android:layout_height="wrap_content" > 6 7 <LinearLayout 8 android:layout_width="match_parent" 9 android:layout_height="match_parent" 10 android:orientation="vertical" > 11 12 <com.zhy.view.RoundImageView 13 android:id="@+id/id_qiqiu" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:layout_margin="10dp" 17 android:src="@drawable/qiqiu" > 18 </com.zhy.view.RoundImageView> 19 20 <com.zhy.view.RoundImageView 21 android:layout_width="200dp" 22 android:layout_height="200dp" 23 android:layout_margin="10dp" 24 android:src="@drawable/aa" > 25 </com.zhy.view.RoundImageView> 26 27 <com.zhy.view.RoundImageView 28 android:layout_width="wrap_content" 29 android:layout_height="wrap_content" 30 android:layout_margin="10dp" 31 android:src="@drawable/icon" > 32 </com.zhy.view.RoundImageView> 33 34 <com.zhy.view.RoundImageView 35 android:id="@+id/id_meinv" 36 android:layout_width="wrap_content" 37 android:layout_height="wrap_content" 38 android:layout_margin="10dp" 39 android:src="@drawable/aa" 40 zhy:borderRadius="20dp" 41 zhy:type="round" > 42 </com.zhy.view.RoundImageView> 43 44 <com.zhy.view.RoundImageView 45 android:layout_width="wrap_content" 46 android:layout_height="wrap_content" 47 android:layout_margin="10dp" 48 android:src="@drawable/icon" 49 zhy:borderRadius="40dp" 50 zhy:type="round" > 51 </com.zhy.view.RoundImageView> 52 53 <com.zhy.view.RoundImageView 54 android:layout_width="wrap_content" 55 android:layout_height="wrap_content" 56 android:layout_margin="10dp" 57 android:src="@drawable/qiqiu" 58 zhy:borderRadius="60dp" 59 zhy:type="round" > 60 </com.zhy.view.RoundImageView> 61 </LinearLayout> 62 63 </ScrollView>
没撒,ScrollView里面一个线性布局,里面一堆RoundImageView。
1 package com.zhy.variousshapeimageview; 2 3 import android.app.Activity; 4 import android.os.Bundle; 5 import android.view.View; 6 import android.view.View.OnClickListener; 7 8 import com.zhy.view.RoundImageView; 9 10 public class MainActivity extends Activity 11 { 12 private RoundImageView mQiQiu; 13 private RoundImageView mMeiNv ; 14 15 @Override 16 protected void onCreate(Bundle savedInstanceState) 17 { 18 super.onCreate(savedInstanceState); 19 setContentView(R.layout.activity_main); 20 21 mQiQiu = (RoundImageView) findViewById(R.id.id_qiqiu); 22 mMeiNv = (RoundImageView) findViewById(R.id.id_meinv); 23 24 mQiQiu.setOnClickListener(new OnClickListener() 25 { 26 @Override 27 public void onClick(View v) 28 { 29 mQiQiu.setType(RoundImageView.TYPE_ROUND); 30 } 31 }); 32 33 mMeiNv.setOnClickListener(new OnClickListener() 34 { 35 36 @Override 37 public void onClick(View v) 38 { 39 mMeiNv.setBorderRadius(90); 40 } 41 }); 42 } 43 44 }
好了,到此本篇博客就结束了。大家可以尝试绘制个五边形或者神马的形状;或者加个边框神马的,相信自己修改应该没问题~~代码可能会存在bug和不足之处,欢迎您的指出,共同进步。
最后的效果图:
源码点击下载