0行代码实现任意形状图片展示--android-anyshape
前言
在Android开发中, 我们经常会遇到一些场景, 需要以一些特殊的形状显示图片, 比如圆角矩形、圆形等等。关于如何绘制这类形状, 网上已经有很多的方案,比如自定义控件重写onDraw方法, 通过canvas的各种draw方法进行绘制等。那么, 更复杂的图形呢?比如,五角星?比如组合图形?又或者是各种奇奇怪怪的不规则图形呢?有同学会说, 如果已知不规则图形的具体形状, 那我们就可以通过连接顶点的方式, 找出path, 然后通过drawPath方法绘制出来啊。嗯。。。很有道理, 但是先不说有些图像,可能顶点巨多, 或者弯弯曲曲很难找出具体的顶点, 难道我们要为每一个特殊的形状, 单独写一个独立的控件, 或者一套独立的代码吗?
可以肯定是可以,但是我觉得, 最好还是不要这么做。。于是我有了一个想法, 用一张图片, 告诉控件,我想要什么样的形状, 然后控件自动按照这个形状, 帮我把图片显示出来。于是有了这个项目--android-anyshape。
展示
左边是使用了普通ImageView的展示效果, 右边是使用了项目中AnyshapeImageView的效果。想使用AnyshapeImageView达到右边的样式, 仅需提供三张遮罩图片,通过”anyshapeMask”参数提供给控件即可(下文会说明)。
三张“遮罩”图片如下:
与普通的遮罩图片不同, 这里要求图片的背景完全透明, 即alpha通道的值为0, 而需要显示的图形,对具体的颜色没有任何要求,不透明即可。
使用
控件的使用很简单, 由于继承ImageView, 所以使用方法类似于ImageView,但多了一个重要的自定义参数:anyshapeMask
<cn.lankton.anyshape.AnyshapeImageView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="20dp"
android:src="@drawable/kumamon"
app:anyshapeMask="@drawable/singlestar"/>
在布局文件中加入这段xml, 展示的就是上面图中那头五角星形状的熊本熊~
实现这个功能的思路其实很简单,通过对一张“遮罩”图片各像素透明度的扫描,获得一个Path对象, 该Path对象包含了所有不透明像素的集合。然后就很简单了, 通过Canvas对象的drawPath方法,将我们要显示的图片刷上去即可。
实现
从Bitmap中提取Path
这是这个项目中最重要的部分。代码如下:
PathInfoManager.getPathFromBitmap:
public Path getPathFromBitmap(Bitmap mask) {
Path path = new Path();
int bWidth = mask.getWidth();
int bHeight = mask.getHeight();
int[] origin = new int[bWidth];
int lastA;
for (int i = 0; i < bHeight; i++) {
mask.getPixels(origin, 0, bWidth, 0, i, bWidth, 1);
lastA = 0;
for (int j = 0; j < bWidth; j++) {
int a = Color.alpha(origin[j]);
if (a != 0 && lastA == 0) {
path.moveTo(j, i);
} else if (a == 0 && lastA !=0 ) {
path.lineTo(j - 1, i);
} else if (a != 0 && j == bWidth - 1) {
path.lineTo(j, i);
}
lastA = a;
}
}
return path;
}
我设计的方案很简单,逐行扫描Bitmap中的像素,实现方法是用getPixels方法获得每行的像素数组,然后遍历分析。步骤如下:
1. 遇到一个不透明像素,进行判断, 如果它的上一个像素不透明, 或者它本身就是行首, 那我们就把它看作一段不透明区域的开头,通过moveTo方法将Path移动到此点;
2. 遇到一个透明像素,进行判断,如果它的上一个像素透明,那我们就把它的上一个像素看作一段不透明区域的结尾, 通过lineTo的方式, 将它与之前的开头像素连接。
3. 重复1、2步, 直到扫描完全行。需要注意的是, 如果行尾是不透明像素, 那就直接连上。防止最后一段不透明区域只有起点没有终点。
这样, 每一行的连接结果,就组成了整张图片的扫描结果~
通过Path,显示图像
先看一下AnyshapeImageView的初始化方法:
public AnyshapeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AnyShapeImageView, defStyleAttr, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
final int attr = a.getIndex(i);
if (attr == R.styleable.AnyShapeImageView_anyshapeMask) {
maskResId = a.getResourceId(attr, 0);
if (0 == maskResId) {
//did not set mask
continue;
}
} else if (attr == R.styleable.AnyShapeImageView_anyshapeBackColor) {
backColor = a.getColor(attr, Color.TRANSPARENT);
}
}
a.recycle();
}
其实就是调用通过anyshapeMask参数, 获得“遮罩”图片的资源ID以及背景色。 真正通过资源ID解析获取遮罩的过程放在了onMeaaure中。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidth = getMeasuredWidth();
int mHeight = getMeasuredHeight();
if (mWidth != 0 && mHeight != 0) {
if (maskResId <= 0) {
return;
}
PathInfo pi = PathManager.getInstance().getPathInfo(maskResId);
if (null != pi) {
originMaskPath = pi.path;
originMaskWidth = pi.width;
originMaskHeight = pi.height;
} else {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(context.getResources(), maskResId, options);
int widthRatio = (int)(options.outWidth * 1f / mWidth);
int heightRatio = (int)(options.outHeight * 1f / mHeight);
if (widthRatio > heightRatio) {
options.inSampleSize = widthRatio;
} else {
options.inSampleSize = heightRatio;
}
if (options.inSampleSize == 0) {
options.inSampleSize = 1;
}
options.inJustDecodeBounds = false;
Bitmap maskBitmap = BitmapFactory.decodeResource(context.getResources(), maskResId, options);
originMaskPath = PathManager.getInstance().getPathFromBitmap(maskBitmap);
originMaskWidth = maskBitmap.getWidth();
originMaskHeight = maskBitmap.getHeight();
pi = new PathInfo();
pi.height = originMaskHeight;
pi.width = originMaskWidth;
pi.path = originMaskPath;
PathManager.getInstance().addPathInfo(maskResId, pi);
maskBitmap.recycle();
}
}
}
PathInfo:
public class PathInfo {
public Path path;
public int width;
public int height;
}
然而我们看到,用户进行生成Bitmap-获取Path这一系列耗时、耗内存操作之前,先会判断缓存里是否已经有与该资源ID匹配的PathInfo, 如果有, 则不用进行这部分操作。如果没有,根据传入的资源ID,生成PathInfo对象,并存入缓存。同时,根据控件的宽高,对decode做了限制,预防了OOM 和 加载资源过大的问题。
关于这块缓存,下面会说明。
再看onSizeChanged方法:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
vHeight = getHeight();
vWidth = getWidth();
if (originMaskPath != null) {
//scale the size of the path to fit the one of this View
Matrix matrix = new Matrix();
matrix.setScale(vWidth * 1f / originMaskWidth, vHeight * 1f / originMaskHeight);
originMaskPath.transform(matrix, realMaskPath);
}
}
这里的代码, 主要目的是对Path对象进行缩放, 已匹配控件的实际大小。可以看到, 如果不希望展示的形状被拉伸或者变形, 那么AnyshapeImageView的宽高比, 最好和“遮罩”图片的宽高比保持一致。
接下来就是在onDraw里绘制形状并刷上图片了:
@Override
protected void onDraw(Canvas canvas) {
if (null == originMaskPath) {
// if the mask is null, the view will work as a normal ImageView
super.onDraw(canvas);
return;
}
if (vWidth == 0 || vHeight == 0) {
return;
}
paint.reset();
paint.setStyle(Paint.Style.STROKE);
//get the drawable to show. if not set the src, will use backColor
Drawable showDrawable = getDrawable();
if (null != showDrawable) {
Bitmap showBitmap = ((BitmapDrawable) showDrawable).getBitmap();
Shader shader = new BitmapShader(showBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Matrix shaderMatrix = new Matrix();
float scaleX = vWidth * 1.0f / showBitmap.getWidth();
float scaleY = vHeight * 1.0f / showBitmap.getHeight();
shaderMatrix.setScale(scaleX, scaleY);
shader.setLocalMatrix(shaderMatrix);
paint.setShader(shader);
} else {
//no src , use the backColor to fill the path
paint.setColor(backColor);
}
canvas.drawPath(realMaskPath, paint);
}
缓存
看了上面的博文, 各位一定清楚了,作为参数传入的资源ID,实际上只是为了获取一个Path对象。那么我们可以建立一个Integer-Path的映射关系, 用来缓存已经读取出来的Path。后面需要Path, 只需要通过资源ID去缓存里寻找即可,毕竟读取Path是一个费时间又费资源的操作。
这样看来,我们已经对AnyshapeImageView的使用进行了优化, 毕竟同一个形状的展示,我们只要执行一次从图片中解析Path对象的操作即可。
总结
这个项目,是我花了将近一周时间断断续续完成的。代码不多, 也不复杂,希望能够帮到大家, 或者为大家提供一些思路。
再贴一下项目的地址, 包括demo在内:
https://github.com/lankton/android-anyshape
如果你觉得这个项目,或者这篇博文对你起到了一些帮助,欢迎star支持一下~
更新
2016-05-12
优化了AnyshapeImageView解析遮罩的过程,PathManager中的createPaths(预先解析Path)变得繁琐且不必要,故删除。 简化后的使用可见项目README。
本次更新后对博文上面代码、讲解内容也有改动。
发布到JCenter-20160519
为方便使用,已将library发布到JCenter,开发者可以使用gradle或者maven进行依赖的配置。
gradle
compile 'cn.lankton:anyshape:1.0.0'
maven
<dependency>
<groupId>cn.lankton</groupId>
<artifactId>anyshape</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>
0行代码实现任意形状图片展示--android-anyshape的更多相关文章
- itest 开源测试管理项目中封装的下拉列表小组件:实现下拉列表使用者前后端0行代码
导读: 主要从4个方面来阐述,1:背景:2:思路:3:代码实现:4:使用 一:封装背景 像easy ui 之类的纯前端组件,也有下拉列表组件,但是使用的时候,每个下拉列表,要配一个URL ...
- Android中绘制圆角矩形图片及任意形状图片
圆角矩形图片在苹果的产品中很流行,相比于普通的矩形,很多人都喜欢圆角矩形的图片,因为它避开了直角的生硬,带来更好的用户体验,下面是几个设计的例子: 下面在Android中实现将普通的矩形图片绘制成圆角 ...
- 33行代码爬取妹子图片(bs4+urllib)
from bs4 import BeautifulSoupimport urllib2import urllibimport lxmlimport os def get_imgs(): image_c ...
- 《第一行代码》学习笔记1-Android系统架构
1. 2003.10,Andy Rubin创办Android公司.2005.8,Google收购之,并于2008年推出Android系统第一个版本. 2. ①Linux Kernel:基于Linux ...
- 《第一行代码》学习笔记2-Android开发特色
1.四大组件:活动(Activity),服务(Service),广播接收器(Broadcast Receiver),内容提供器(Content Provider). Activity:应用中看得到的东 ...
- swing重绘按钮为任意形状图案的方法
swing重绘按钮为任意形状图案的方法 摘自https://www.jb51.net/article/131290.htm 转载 更新时间:2017年12月22日 13:43:00 作者:_Th ...
- Android Studio 单刷《第一行代码》系列目录
前言(Prologue) 本系列将使用 Android Studio 将<第一行代码>(书中讲解案例使用Eclipse)刷一遍,旨在为想入坑 Android 开发,并选择 Android ...
- 仿照微信的效果,实现了一个支持多选、选原图和视频的图片选择器,适配了iOS6-9系统,3行代码即可集成.
提示:如果你发现了Bug,请尝试更新到最新版.目前最新版是1.6.4,此前的版本或多或少存在一些bug的~如果你已经是最新版了,请留一条评论,我看到了会尽快处理和修复哈~ 关于升级iOS10和Xcdo ...
- 5行代码实现微信小程序图片上传与腾讯免费5G存储空间的使用
本文介绍了如何在微信小程序开发中使用腾讯官方提供的云开发功能快速实现图片的上传与存储,以及介绍云开发的 5G 存储空间的基本使用方法,这将大大提高微信小程序的开发效率,同时也是微信小程序系列教程的视频 ...
随机推荐
- CentOS下安装Python3
目录 CentOS下安装Python3 下载 解压 配置 gcc sudo权限 vim 编译 安装 添加软链接 pip安装出错,找不到SSL 安装virtualenv和virtualenvwrappe ...
- array_pop()方法
array_pop — 将数组最后一个单元弹出(出栈) 说明 mixed array_pop ( array &$array ) array_pop() 弹出并返回 array 数组的最后一个 ...
- 在GIT 中增加忽略文件夹与文件
1,在工作目录点右建选择 2,输入touch .gitignore 在工作目录就生成了一个“.gitignore”文件. 3,然后在”.gitignore” 文件里输入你要忽略的文件夹及其文件就可以了 ...
- Dog test1 = new Dog()的解释
- Java 8新特性之lambda(八恶人-2)
Major Marquis Warren 沃伦·马奎斯少校 “Tring to get a couple of bounties in to Red Rock.”我想带几个通缉犯去红石镇 一.基本介绍 ...
- BZOJ 3993 [SDOI2015]星际战争 | 网络流 二分答案
链接 BZOJ 3993 题解 这道题挺棵的-- 二分答案t,然后源点向武器连t * b[i], 武器向能攻击的敌人连1, 敌人向汇点连a[i],如果最大流等于所有敌人的a[i]之和则可行. #inc ...
- 解决 winform 界面对不齐
最近做了一个winform的程序,本机上界面对得很齐,到一到客户的机器上就惨不忍睹,一番研究后搞定: 1. AutoScaleMode = None 2. BackgroundImageLayout ...
- [JSOI2008]魔兽地图
Description DotR里面的英雄只有一个属性——力量. 他们需要购买装备来提升自己的力量值,每件装备都可以使佩戴它的英雄的力量值提高固定的点数,所以英雄的力量值等于它购买的所有装备的力量值之 ...
- Objective-C 中的协议(@protocol)和接口(@interface)的区别
Objective-C 中的协议(@protocol),依照我的理解,就是C#, Java, Pascal等语言中的接口(Interface).协议本身不实现任何方法,只是声明方法,使用协议的类必须实 ...
- Eclipse Neon安装指导
[下载] 前往Eclipse官网:http://www.eclipse.org/,点击DOWNLOAD: 进入下载页面后,会显示如下下载界面: 找到 Get Eclipse Neon,然后点击下面的” ...