Android canvas: нарисуйте прозрачный круг на изображении
Я создаю игру охоты на пикселов. Так что моя активность показывает ImageView. И я хочу создать подсказку "покажи мне, где находится объект". Для этого мне нужно размыть все изображение, кроме круга вокруг точки, где расположен объект. Вместо размытия я могу показать просто полупрозрачный черный фон. Там нет проблем нарисовать полупрозрачный прямоугольник на холсте. Но я не знаю, как вырезать из него прозрачный круг. Результат должен выглядеть примерно так:
Пожалуйста, помогите мне достичь того же результата на Android SDK.
11 ответов
Наконец-то мне удалось это сделать.
Сначала я рисую полупрозрачный черный прямоугольник на всем виде. После этого с помощью PorterDuff.Mode.CLEAR
Я вырезал прозрачный круг, чтобы показать положение кошки.
У меня были проблемы с PorterDuff.Mode.CLEAR
: сначала я получил черный круг вместо прозрачного.
Благодаря комментариям Ромена Гая здесь: комментарий здесь, я понял, что мое окно непрозрачно, и я должен нарисовать на другом растровом изображении. И только после розыгрыша View
Холст
Вот мой onDraw
метод:
private Canvas temp;
private Paint paint;
private Paint p = new Paint();
private Paint transparentPaint;
private void init(){
temp = new Canvas(bitmap);
Bitmap bitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
paint = new Paint();
paint.setColor(0xcc000000);
transparentPaint = new Paint();
transparentPaint.setColor(getResources().getColor(android.R.color.transparent));
transparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
protected void onDraw(Canvas canvas) {
temp.drawRect(0, 0, temp.getWidth(), temp.getHeight(), paint);
temp.drawCircle(catPosition.x + radius / 2, catPosition.y + radius / 2, radius, transparentPaint);
canvas.drawBitmap(bitmap, 0, 0, p);
}
Я нашел решение без растрового рисования и создания. Вот результат моей реализации:
Вам нужно создать кастом FrameLayout
а также drawCircle
с Clear
покрасить:
mBackgroundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
Также не забудьте отключить аппаратное ускорение и позвонить setWillNotDraw(false)
потому что мы переопределим onDraw
метод
setWillNotDraw(false);
setLayerType(LAYER_TYPE_HARDWARE, null);
Полный пример здесь:
public class TutorialView extends FrameLayout {
private static final float RADIUS = 200;
private Paint mBackgroundPaint;
private float mCx = -1;
private float mCy = -1;
private int mTutorialColor = Color.parseColor("#D20E0F02");
public TutorialView(Context context) {
super(context);
init();
}
public TutorialView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TutorialView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TutorialView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setWillNotDraw(false);
setLayerType(LAYER_TYPE_HARDWARE, null);
mBackgroundPaint = new Paint();
mBackgroundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mCx = event.getX();
mCy = event.getY();
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(mTutorialColor);
if (mCx >= 0 && mCy >= 0) {
canvas.drawCircle(mCx, mCy, RADIUS, mBackgroundPaint);
}
}
}
PS: эта реализация просто рисует дыру внутри себя, вам нужно поместить фон в макет и добавить это TutorialView
наверху.
Я сделал это путем создания пользовательских LinearLayout:
Проверьте скриншот:
CircleOverlayView.java
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.LinearLayout;
/**
* Created by hiren on 10/01/16.
*/
public class CircleOverlayView extends LinearLayout {
private Bitmap bitmap;
public CircleOverlayView(Context context) {
super(context);
}
public CircleOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CircleOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CircleOverlayView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (bitmap == null) {
createWindowFrame();
}
canvas.drawBitmap(bitmap, 0, 0, null);
}
protected void createWindowFrame() {
bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas osCanvas = new Canvas(bitmap);
RectF outerRectangle = new RectF(0, 0, getWidth(), getHeight());
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(getResources().getColor(R.color.colorPrimary));
paint.setAlpha(99);
osCanvas.drawRect(outerRectangle, paint);
paint.setColor(Color.TRANSPARENT);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
float centerX = getWidth() / 2;
float centerY = getHeight() / 2;
float radius = getResources().getDimensionPixelSize(R.dimen.radius);
osCanvas.drawCircle(centerX, centerY, radius, paint);
}
@Override
public boolean isInEditMode() {
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
bitmap = null;
}
}
CircleDrawActivity.java:
public class CircleDrawActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_circle_draw);
}
}
activity_circle_draw.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rlParent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/lighthouse"
android:scaleType="fitXY" />
<common.customview.CircleOverlayView
android:id="@+id/cicleOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent">
</common.customview.CircleOverlayView>
</RelativeLayout>
colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>
Dimens.xml:
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="nav_header_vertical_spacing">16dp</dimen>
<dimen name="nav_header_height">160dp</dimen>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="radius">50dp</dimen>
</resources>
Надеюсь, что это поможет вам.
Ответ Роберта на самом деле показал мне, как решить эту проблему, но его код не работает. Поэтому я обновил его решение и заставил его работать:
public class CaptureLayerView extends View {
private Bitmap bitmap;
private Canvas cnvs;
private Paint p = new Paint();
private Paint transparentPaint = new Paint();;
private Paint semiTransparentPaint = new Paint();;
private int parentWidth;
private int parentHeight;
private int radius = 100;
public CaptureLayerView(Context context) {
super(context);
init();
}
public CaptureLayerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
transparentPaint.setColor(getResources().getColor(android.R.color.transparent));
transparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
semiTransparentPaint.setColor(getResources().getColor(R.color.colorAccent));
semiTransparentPaint.setAlpha(70);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
bitmap = Bitmap.createBitmap(parentWidth, parentHeight, Bitmap.Config.ARGB_8888);
cnvs = new Canvas(bitmap);
cnvs.drawRect(0, 0, cnvs.getWidth(), cnvs.getHeight(), semiTransparentPaint);
cnvs.drawCircle(parentWidth / 2, parentHeight / 2, radius, transparentPaint);
canvas.drawBitmap(bitmap, 0, 0, p);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
parentWidth = MeasureSpec.getSize(widthMeasureSpec);
parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth, parentHeight);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
А теперь используйте этот вид в любом макете, как это:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
</SurfaceView>
<com.example.myapp.CaptureLayerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btnTakePicture"
android:layout_width="match_parent"
android:layout_height="80dp"
android:onClick="onClickPicture"
android:text="@string/take_picture">
</Button>
Здесь я хотел полупрозрачный слой на SurfaceView с прозрачным кругом в центре.PS Этот код не оптимизирован, поскольку он создает растровое изображение в методе onDraw, потому что я не смог получить ширину и высоту родительского представления в методе init, поэтому я мог знать их только в onDraw.
Простое решение 2020 kotlin без каких-либо предупреждений.
1. Код для настраиваемого вида с отверстием
class HolePosition(var x: Float, var y: Float, var r: Float)
class HoleView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): View(context, attrs, defStyleAttr) {
private val paint: Paint = Paint()
private var holePaint: Paint = Paint()
private var bitmap: Bitmap? = null
private var layer: Canvas? = null
//position of hole
var holePosition: HolePosition = HolePosition(0.0f, 0.0f, 0.0f)
set(value) {
field = value
//redraw
this.invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (bitmap == null) { configureBitmap() }
//draw background
layer?.drawRect(0.0f, 0.0f, width.toFloat(), height.toFloat(), paint)
//draw hole
layer?.drawCircle(holePosition.x, holePosition.y, holePosition.r, holePaint);
//draw bitmap
canvas.drawBitmap(bitmap!!, 0.0f, 0.0f, paint);
}
private fun configureBitmap() {
//create bitmap and layer
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
layer = Canvas(bitmap!!)
}
init {
//configure background color
val backgroundAlpha = 0.8
paint.color = ColorUtils.setAlphaComponent(resources.getColor(R.color.mainDark, null), (255 * backgroundAlpha).toInt() )
//configure hole color & mode
holePaint.color = resources.getColor(android.R.color.transparent, null)
holePaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
}
2. Пример макета
<com.your_company.package.HoleView
android:id="@+id/hole_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.your_company.package.HoleView>
3. Использование набора отверстий
val holeView = findViewById<HoleView>(R.id.hole_view)
holeView.holePosition = HolePosition(x = 350.0f, y = 350.0f, r = 180.0f)
4. Результаты
На самом деле это можно сделать гораздо более эффективным способом и с гораздо меньшим количеством кода, используя объект Path.
код: В конструкторе класса наложения View:
path.addCircle(600,1000, 200, Path.Direction.CW);
path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
paint.setColor(0x55_00_00_00);
(Числа для x, y и радиуса выше должны быть заменены тем, что вам нужно).
И переопределите представление onDraw(Canvas):
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(path,paint);
}
Вот и все...
Мне нечего добавить к вашему ответу, но если кому-то интересно, я переместил распределение растровых изображений и все в onSizeChanged, чтобы повысить производительность.
Здесь вы можете найти FrameLayout с "отверстием" в середине;)
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.FrameLayout;
/**
* Created by blackvvine on 1/1/16.
*/
public class SteroidFrameLayout extends FrameLayout {
private Paint transPaint;
private Paint defaultPaint;
private Bitmap bitmap;
private Canvas temp;
public SteroidFrameLayout(Context context) {
super(context);
__init__();
}
public SteroidFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
__init__();
}
public SteroidFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
__init__();
}
private void __init__() {
transPaint = new Paint();
defaultPaint = new Paint();
transPaint.setColor(Color.TRANSPARENT);
transPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
setWillNotDraw(false);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
temp = new Canvas(bitmap);
}
@Override
protected void dispatchDraw(Canvas canvas) {
temp.drawColor(Color.TRANSPARENT);
super.dispatchDraw(temp);
temp.drawCircle(cx, cy, getWidth()/4, transPaint);
canvas.drawBitmap(bitmap, 0, 0, defaultPaint);
if (p < 1)
invalidate();
else
animRunning = false;
}
}
PS: хотя он намного эффективнее, чем оригинальный ответ, это все же является относительно сложной задачей в методе draw(), поэтому, если вы используете эту технику в анимации, подобной мне, не ожидайте, что сглаживание будет 60.0fps
public class CircleBlur extends Activity implements View.OnTouchListener {
SeekBar seekBar;
ImageView image,image1;
private Paint paint;
Bitmap circle,blurimg;
private Matrix matrix = new Matrix();
private Matrix savedMatrix = new Matrix();
private static final int NONE = 0;
private static final int DRAG = 1;
private static final int ZOOM = 2;
private int mode = NONE;
private PointF start = new PointF();
private PointF mid = new PointF();
private float oldDist = 1f;
float newRot = 0f;
private float d = 0f;
private float[] lastEvent = null;
private float radius=12;
Bitmap blurbitmap;
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_blurimage);
image=findViewById(R.id.image);
seekBar=findViewById(R.id.seekbar);
image1=findViewById(R.id.image1);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//here your image bind to Imageview
image.setImageResource(R.drawable.nas1);
image1.setOnTouchListener(this);
seekBar.setProgress(12);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
radius = (float) CircleBlur.this.seekBar.getProgress();
blurbitmap=createBlurBitmap(blurimg, radius);
CircleBlur();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
private Bitmap createBlurBitmap(Bitmap src, float r) {
if (r <= 0) {
r = 0.1f;
} else if (r > 25) {
r = 25.0f;
}
Bitmap bitmap = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);
RenderScript renderScript = RenderScript.create(this);
Allocation blurInput = Allocation.createFromBitmap(renderScript, src);
Allocation blurOutput = Allocation.createFromBitmap(renderScript, bitmap);
ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
blur.setInput(blurInput);
blur.setRadius(r);
blur.forEach(blurOutput);
blurOutput.copyTo(bitmap);
renderScript.destroy();
return bitmap;
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
public void CircleBlur()
{
Bitmap result;
// your circle image
circle = BitmapFactory.decodeResource(getResources(),R.drawable.cicleouter);
result = Bitmap.createBitmap(blurimg.getWidth(), blurimg.getHeight(), Bitmap.Config.ARGB_8888);
Canvas mCanvas = new Canvas(result);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
paint.setDither(true);
mCanvas.drawBitmap(blurimg,0,0, null);
mCanvas.drawBitmap(circle, matrix, paint);
paint.setXfermode(null);
image1.setImageBitmap(result);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public boolean onTouch(View v, MotionEvent event) {
image1 = (ImageView) v;
float x = event.getX(), y = event.getY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
savedMatrix.set(matrix);
start.set(x, y);
mode = DRAG;
lastEvent = null;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
if (oldDist > 10f) {
savedMatrix.set(matrix);
midPoint(mid, event);
mode = ZOOM;
}
lastEvent = new float[4];
lastEvent[0] = event.getX(0);
lastEvent[1] = event.getX(1);
lastEvent[2] = event.getY(0);
lastEvent[3] = event.getY(1);
d = rotation(event);
break;
// case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
lastEvent = null;
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG) {
matrix.set(savedMatrix);
float dx = x - start.x;
float dy = y - start.y;
matrix.postTranslate(dx, dy);
} else if (mode == ZOOM) {
float newDist = spacing(event);
if (newDist > 10f) {
matrix.set(savedMatrix);
float scale = (newDist / oldDist);
matrix.postScale(scale, scale, mid.x, mid.y);
}
if (lastEvent != null && event.getPointerCount() == 2 || event.getPointerCount() == 3) {
newRot = rotation(event);
float r = newRot - d;
float[] values = new float[9];
matrix.getValues(values);
float tx = values[2];
float ty = values[5];
float sx = values[0];
float xc = (image.getWidth() / 2) * sx;
float yc = (image.getHeight() / 2) * sx;
matrix.postRotate(r, tx + xc, ty + yc);
}
}
break;
}
CircleBlur();
return true;
}
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
float s=x * x + y * y;
return (float)Math.sqrt(s);
}
private void midPoint(PointF point, MotionEvent event) {
float x = event.getX(0) + event.getX(1);
float y = event.getY(0) + event.getY(1);
point.set(x / 2, y / 2);
}
private float rotation(MotionEvent event) {
double delta_x = (event.getX(0) - event.getX(1));
double delta_y = (event.getY(0) - event.getY(1));
double radians = Math.atan2(delta_y, delta_x);
return (float) Math.toDegrees(radians);
}
}
Версия Kotlin без выделения Bitmap. Но вместо круга здесь нарисован скругленный прямоугольник. Я уверен, что вы сможете адаптировать его к кругу самостоятельно.
private val scanAreaBorder = Paint().apply {
color = ContextCompat.getColor(context, R.color.postyellow)
style = Paint.Style.STROKE
strokeWidth = dpToPx(context, resources.getDimension(R.dimen.scan_area_border_stroke_width).toInt())
}
private val scanArea = Paint().apply {
color = ContextCompat.getColor(context, android.R.color.transparent)
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
private val background = Paint().apply {
color = ContextCompat.getColor(context, R.color.scanner_area)
}
init {
setLayerType(LAYER_TYPE_HARDWARE, null)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.run {
drawRect(0f, 0f, width.toFloat(), height.toFloat(), background)
val top = (height / 2f) - dpToPx(context, resources.getDimension(R.dimen.scan_area_height).toInt())
val bottom = (height / 2f) + dpToPx(context, resources.getDimension(R.dimen.scan_area_height).toInt())
val margin = dpToPx(context, resources.getDimension(R.dimen.scan_area_margin).toInt())
val cornerRadius = dpToPx(context, resources.getDimension(R.dimen.scan_area_corner_radius).toInt())
drawRoundRect(margin, top, width.toFloat() - margin, bottom, cornerRadius, cornerRadius, scanArea)
drawRoundRect(margin, top, width.toFloat() - margin, bottom, cornerRadius, cornerRadius, scanAreaBorder)
}
}
Это сработало для меня:
canvas.drawCircle(x,y,radius,new Paint(Color.TRANSPARENT))
Если у вас возникли проблемы с наложением прозрачного круга на вид с непрозрачным фоном, посмотрите этот ответ. Чтобы заставить его работать, я настроил свой настраиваемый вид макета на прозрачный фон в XML, а затем нарисовал цвет фона, который хотел для макета, с линией
cv.drawColor(Color.BLUE); //replace with your desired background color
Полный метод OnDraw из ответа, который я связал выше:
@Override
protected void onDraw(Canvas canvas) {
int w = getWidth();
int h = getHeight();
int radius = w > h ? h / 2 : w / 2;
bm.eraseColor(Color.TRANSPARENT);
cv.drawColor(Color.BLUE);
cv.drawCircle(w / 2, h / 2, radius, eraser);
canvas.drawBitmap(bm, 0, 0, null);
super.onDraw(canvas);
}