Android 自定义图像视图形状
Android custom image view shape
我正在创建一个自定义 ImageView,它将我的图像裁剪成六边形并添加边框。我想知道我的方法是否正确,或者我是否以错误的方式这样做。那里有很多自定义库已经可以做到这一点,但是 none 开箱即用的库具有我正在寻找的形状。话虽如此,这更多是关于最佳实践的问题。
您可以在 gist 中看到完整的 class,但主要问题是这是最好的方法。我觉得不对劲,部分原因是一些神奇的数字意味着它可能会在某些设备上搞砸。
这是代码的核心:
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
if (drawable == null || getWidth() == 0 || getHeight() == 0) {
return;
}
Bitmap b = ((BitmapDrawable) drawable).getBitmap();
Bitmap bitmap = b.copy(Bitmap.Config.ARGB_8888, true);
int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.width); // (width and height of ImageView)
Bitmap drawnBitmap = drawCanvas(bitmap, dimensionPixelSize);
canvas.drawBitmap(drawnBitmap, 0, 0, null);
}
private Bitmap drawCanvas(Bitmap recycledBitmap, int width) {
final Bitmap bitmap = verifyRecycledBitmap(recycledBitmap, width);
final Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final Rect rect = new Rect(0, 0, width, width);
final int offset = (int) (width / (double) 2 * Math.tan(30 * Math.PI / (double) 180)); // (width / 2) * tan(30deg)
final int length = width - (2 * offset);
final Path path = new Path();
path.moveTo(width / 2, 0); // top
path.lineTo(0, offset); // left top
path.lineTo(0, offset + length); // left bottom
path.lineTo(width / 2, width); // bottom
path.lineTo(width, offset + length); // right bottom
path.lineTo(width, offset); // right top
path.close(); //back to top
Paint paint = new Paint();
paint.setStrokeWidth(4);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawPath(path, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint); // draws the bitmap for the image
paint.setColor(Color.parseColor("white"));
paint.setStrokeWidth(4);
paint.setDither(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setPathEffect(new CornerPathEffect(10));
paint.setAntiAlias(true); // draws the border
canvas.drawPath(path, paint);
return output;
}
我正在查看一些 iOS 代码,他们能够应用实际图像作为蒙版来实现此结果。 Android 上有没有类似的东西?
尽管它可能有效,但此实现中存在一些严重错误:
您在 onDraw
阶段分配了一些非常大的对象,这导致了糟糕的性能。最重要的是 createBitmap
,但您应该在 onDraw
期间不惜一切代价避免任何 new
。在初始化期间预分配所有必要的对象并在 onDraw
.
期间重新使用它们
您应该在 onSizeChanged
期间只设置一次 path
。避开每个 OnDraw
上的所有路径
你依赖于 BitmapDrawable
的用法,例如,如果你使用 Picasso
从互联网加载图像,或者你想使用选择器,那么这段代码不行。
您不需要分配第二个位图,使用 canvas.clipPath
来提高效率。
都说了一个更高效的绘图伪代码应该是:
@Override
protected void onDraw(Canvas canvas) {
canvas.save(CLIP_SAVE_FLAG); // save the clipping
canvas.clipPath(path, Region.Op./*have to test which one*/ ); // cut the canvas
super.onDraw(canvas); // do the normal drawing
canvas.restore(); // restore the saved clipping
canvas.drawPath(path, paint); // draw the extra border
}
我一直在寻找最好的方法。您的解决方案非常繁重,不适用于动画。 clipPath 方法不使用抗锯齿功能,并且在 Android(4.0 和 4.1?)的某些版本上不适用于硬件加速。似乎最好的方法(动画友好、抗锯齿、非常干净和硬件加速)是使用 Canvas 层:
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private static PorterDuffXfermode pdMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
@Override
public void draw(Canvas canvas) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(),
null, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
paint.setXfermode(pdMode);
canvas.drawBitmap(maskBitmap, 0, 0, paint);
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}
您可以使用任何类型的遮罩,包括自定义形状和位图。 Carbon 使用这种方法动态地对小部件进行圆角处理。
在build.gradle中添加依赖
implementation 'com.github.siyamed:android-shape-imageview:0.9.+@aar'
implementation group: 'net.sf.kxml', name: 'kxml2-min', version: '2.3.0'
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<com.github.siyamed.shapeimageview.HexagonImageView
android:id="@+id/imageView"
android:layout_marginTop="100dp"
android:layout_width="250dp"
android:layout_height="350dp" />
</LinearLayout>
activity
public class MainActivity extends AppCompatActivity {
HexagonImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView=findViewById(R.id.imageView);
imageView.setImageResource(R.drawable.img);
}}
输出
在build.gradle
中添加依赖
implementation 'com.github.siyamed:android-shape-imageview:0.9.+@aar'
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<com.github.siyamed.shapeimageview.mask.PorterShapeImageView
android:id="@+id/imageView"
android:layout_width="250dp"
android:layout_height="350dp"
app:siShape="@drawable/shape" />
</LinearLayout>
activity
public class MainActivity extends AppCompatActivity {
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView=findViewById(R.id.imageView);
imageView.setImageResource(R.drawable.img);
}
}
可绘制资源
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFC107" />
<padding android:left="7dp"
android:top="7dp"
android:right="7dp"
android:bottom="7dp" />
<corners
android:topLeftRadius="0dip"
android:topRightRadius="70dip"
android:bottomLeftRadius="70dip"
android:bottomRightRadius="0dip" />
</shape>
产出
我正在创建一个自定义 ImageView,它将我的图像裁剪成六边形并添加边框。我想知道我的方法是否正确,或者我是否以错误的方式这样做。那里有很多自定义库已经可以做到这一点,但是 none 开箱即用的库具有我正在寻找的形状。话虽如此,这更多是关于最佳实践的问题。
您可以在 gist 中看到完整的 class,但主要问题是这是最好的方法。我觉得不对劲,部分原因是一些神奇的数字意味着它可能会在某些设备上搞砸。
这是代码的核心:
@Override protected void onDraw(Canvas canvas) { Drawable drawable = getDrawable(); if (drawable == null || getWidth() == 0 || getHeight() == 0) { return; } Bitmap b = ((BitmapDrawable) drawable).getBitmap(); Bitmap bitmap = b.copy(Bitmap.Config.ARGB_8888, true); int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.width); // (width and height of ImageView) Bitmap drawnBitmap = drawCanvas(bitmap, dimensionPixelSize); canvas.drawBitmap(drawnBitmap, 0, 0, null); } private Bitmap drawCanvas(Bitmap recycledBitmap, int width) { final Bitmap bitmap = verifyRecycledBitmap(recycledBitmap, width); final Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(output); final Rect rect = new Rect(0, 0, width, width); final int offset = (int) (width / (double) 2 * Math.tan(30 * Math.PI / (double) 180)); // (width / 2) * tan(30deg) final int length = width - (2 * offset); final Path path = new Path(); path.moveTo(width / 2, 0); // top path.lineTo(0, offset); // left top path.lineTo(0, offset + length); // left bottom path.lineTo(width / 2, width); // bottom path.lineTo(width, offset + length); // right bottom path.lineTo(width, offset); // right top path.close(); //back to top Paint paint = new Paint(); paint.setStrokeWidth(4); canvas.drawARGB(0, 0, 0, 0); canvas.drawPath(path, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(bitmap, rect, rect, paint); // draws the bitmap for the image paint.setColor(Color.parseColor("white")); paint.setStrokeWidth(4); paint.setDither(true); paint.setStyle(Paint.Style.STROKE); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeCap(Paint.Cap.ROUND); paint.setPathEffect(new CornerPathEffect(10)); paint.setAntiAlias(true); // draws the border canvas.drawPath(path, paint); return output; }
我正在查看一些 iOS 代码,他们能够应用实际图像作为蒙版来实现此结果。 Android 上有没有类似的东西?
尽管它可能有效,但此实现中存在一些严重错误:
您在
onDraw
阶段分配了一些非常大的对象,这导致了糟糕的性能。最重要的是createBitmap
,但您应该在onDraw
期间不惜一切代价避免任何new
。在初始化期间预分配所有必要的对象并在onDraw
. 期间重新使用它们
您应该在
onSizeChanged
期间只设置一次path
。避开每个OnDraw
上的所有路径
你依赖于
BitmapDrawable
的用法,例如,如果你使用Picasso
从互联网加载图像,或者你想使用选择器,那么这段代码不行。您不需要分配第二个位图,使用
canvas.clipPath
来提高效率。
都说了一个更高效的绘图伪代码应该是:
@Override
protected void onDraw(Canvas canvas) {
canvas.save(CLIP_SAVE_FLAG); // save the clipping
canvas.clipPath(path, Region.Op./*have to test which one*/ ); // cut the canvas
super.onDraw(canvas); // do the normal drawing
canvas.restore(); // restore the saved clipping
canvas.drawPath(path, paint); // draw the extra border
}
我一直在寻找最好的方法。您的解决方案非常繁重,不适用于动画。 clipPath 方法不使用抗锯齿功能,并且在 Android(4.0 和 4.1?)的某些版本上不适用于硬件加速。似乎最好的方法(动画友好、抗锯齿、非常干净和硬件加速)是使用 Canvas 层:
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private static PorterDuffXfermode pdMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
@Override
public void draw(Canvas canvas) {
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(),
null, Canvas.ALL_SAVE_FLAG);
super.draw(canvas);
paint.setXfermode(pdMode);
canvas.drawBitmap(maskBitmap, 0, 0, paint);
canvas.restoreToCount(saveCount);
paint.setXfermode(null);
}
您可以使用任何类型的遮罩,包括自定义形状和位图。 Carbon 使用这种方法动态地对小部件进行圆角处理。
在build.gradle中添加依赖
implementation 'com.github.siyamed:android-shape-imageview:0.9.+@aar'
implementation group: 'net.sf.kxml', name: 'kxml2-min', version: '2.3.0'
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<com.github.siyamed.shapeimageview.HexagonImageView
android:id="@+id/imageView"
android:layout_marginTop="100dp"
android:layout_width="250dp"
android:layout_height="350dp" />
</LinearLayout>
activity
public class MainActivity extends AppCompatActivity {
HexagonImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView=findViewById(R.id.imageView);
imageView.setImageResource(R.drawable.img);
}}
输出
在build.gradle
中添加依赖implementation 'com.github.siyamed:android-shape-imageview:0.9.+@aar'
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<com.github.siyamed.shapeimageview.mask.PorterShapeImageView
android:id="@+id/imageView"
android:layout_width="250dp"
android:layout_height="350dp"
app:siShape="@drawable/shape" />
</LinearLayout>
activity
public class MainActivity extends AppCompatActivity {
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView=findViewById(R.id.imageView);
imageView.setImageResource(R.drawable.img);
}
}
可绘制资源
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFC107" />
<padding android:left="7dp"
android:top="7dp"
android:right="7dp"
android:bottom="7dp" />
<corners
android:topLeftRadius="0dip"
android:topRightRadius="70dip"
android:bottomLeftRadius="70dip"
android:bottomRightRadius="0dip" />
</shape>
产出