最终效果图:
实现方案:自定义一个父容器RelativeLayout将ImageView放入父容器中并初始化一个和ImageView相同大小的DrawingView来做涂鸦层最后将ImageView和DrawingView重叠部分生成Bitmap。
父容器代码PhotoEditorView:
public class PhotoEditorView extends RelativeLayout {
private ImageView mImgSource;
private BrushDrawingView mBrushDrawingView;
private static final int imgSrcId = 1, brushSrcId = 2;
public PhotoEditorView(Context context) {
super(context);
init();
}
public PhotoEditorView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PhotoEditorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PhotoEditorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
//Setup image attributes
mImgSource = new ImageView(getContext());
mImgSource.setId(imgSrcId);
mImgSource.setAdjustViewBounds(true);
LayoutParams imgSrcParam = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
imgSrcParam.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
//Setup brush view
mBrushDrawingView = new BrushDrawingView(getContext());
mBrushDrawingView.setVisibility(GONE);
mBrushDrawingView.setId(brushSrcId);
//Align brush to the size of image view
LayoutParams brushParam = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
brushParam.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
brushParam.addRule(RelativeLayout.ALIGN_TOP, imgSrcId);
brushParam.addRule(RelativeLayout.ALIGN_BOTTOM, imgSrcId);
//Add image source
addView(mImgSource, imgSrcParam);
//Add brush view
addView(mBrushDrawingView, brushParam);
}
/**
* Source image which you want to edit
*
* @return source ImageView
*/
public ImageView getImageView() {
return mImgSource;
}
BrushDrawingView getBrushDrawingView() {
return mBrushDrawingView;
}
/**
* 返回最终Bitmap
*
* @return
*/
public Bitmap getResultBitmap() {
BitmapDrawable drawable = (BitmapDrawable) mImgSource.getDrawable();
Bitmap imageViewBitmap = drawable.getBitmap();
RectF clipRect = new RectF();
clipRect.top = mImgSource.getY();
clipRect.left = mImgSource.getX();
clipRect.bottom = mImgSource.getHeight();
clipRect.right = mImgSource.getWidth();
PointF srcSize = new PointF();
srcSize.x = imageViewBitmap.getWidth();
srcSize.y = imageViewBitmap.getHeight();
Bitmap bitmap = mBrushDrawingView.getBrushResultImage(clipRect, srcSize);
Bitmap resultBitmap = Bitmap.createBitmap(imageViewBitmap.getWidth(), imageViewBitmap.getHeight(), Bitmap.Config
.ARGB_4444);
Canvas canvas = new Canvas(resultBitmap);
canvas.drawBitmap(imageViewBitmap, 0, 0, null);
canvas.drawBitmap(bitmap, 0, 0, null);
return resultBitmap;
}
}
自定义涂鸦path View代码:
public class BrushDrawingView extends View {
private float mBrushSize = 15;
private int mOpacity = 255;
private List<LinePath> mLinePaths = new ArrayList<>();
private Paint mDrawPaint;
private Canvas mDrawCanvas;
private boolean mBrushDrawMode;
private Path mPath;
private float mTouchX, mTouchY;
private static final float TOUCH_TOLERANCE = 4;
public BrushDrawingView(Context context) {
this(context, null);
}
public BrushDrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
setupBrushDrawing();
}
public BrushDrawingView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setupBrushDrawing();
}
void setupBrushDrawing() {
//Caution: This line is to disable hardware acceleration to make eraser feature work properly
setLayerType(LAYER_TYPE_HARDWARE, null);
mDrawPaint = new Paint();
mPath = new Path();
mDrawPaint.setAntiAlias(true);
mDrawPaint.setDither(true);
mDrawPaint.setColor(Color.RED);
mDrawPaint.setStyle(Paint.Style.STROKE);
mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
mDrawPaint.setStrokeWidth(mBrushSize);
mDrawPaint.setAlpha(mOpacity);
mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
this.setVisibility(View.GONE);
}
private void refreshBrushDrawing() {
mBrushDrawMode = true;
mPath = new Path();
mDrawPaint.setAntiAlias(true);
mDrawPaint.setDither(true);
mDrawPaint.setStyle(Paint.Style.STROKE);
mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
mDrawPaint.setStrokeWidth(mBrushSize);
mDrawPaint.setAlpha(mOpacity);
mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
}
void setBrushDrawingMode(boolean brushDrawMode) {
this.mBrushDrawMode = brushDrawMode;
if (brushDrawMode) {
this.setVisibility(View.VISIBLE);
refreshBrushDrawing();
}
}
void setOpacity(@IntRange(from = 0, to = 255) int opacity) {
this.mOpacity = opacity;
setBrushDrawingMode(true);
}
boolean getBrushDrawingMode() {
return mBrushDrawMode;
}
boolean isCacheEmpty() {
return mLinePaths.isEmpty();
}
void setBrushSize(float size) {
mBrushSize = size;
setBrushDrawingMode(true);
}
void setBrushColor(@ColorInt int color) {
mDrawPaint.setColor(color);
setBrushDrawingMode(true);
}
float getBrushSize() {
return mBrushSize;
}
int getBrushColor() {
return mDrawPaint.getColor();
}
void clearAll() {
mLinePaths.clear();
if (mDrawCanvas != null) {
mDrawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0) {
Bitmap canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
mDrawCanvas = new Canvas(canvasBitmap);
}
}
@Override
protected void onDraw(Canvas canvas) {
for (LinePath linePath : mLinePaths) {
canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
}
canvas.drawPath(mPath, mDrawPaint);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (mBrushDrawMode) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchStart(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
touchMove(touchX, touchY);
break;
case MotionEvent.ACTION_UP:
touchUp();
break;
}
invalidate();
return true;
} else {
return false;
}
}
public Bitmap getBrushResultImage(RectF clipRect, PointF srcSize) {
Bitmap drawBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
Canvas canvas = new Canvas(drawBitmap);
canvas.drawColor(Color.TRANSPARENT);
for (int i = 0; i < mLinePaths.size(); i++) {
LinePath linePath = mLinePaths.get(i);
canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
}
Bitmap clipBitmap = Bitmap.createBitmap(drawBitmap, 0, 0, (int) clipRect.right, (int) clipRect.bottom, null,
false);
Bitmap resultBitmap = Bitmap.createScaledBitmap(clipBitmap, (int) srcSize.x, (int) srcSize.y, true);
drawBitmap.recycle();
clipBitmap.recycle();
return resultBitmap;
}
private class LinePath {
private Paint mDrawPaint;
private Path mDrawPath;
LinePath(Path drawPath, Paint drawPaints) {
mDrawPaint = new Paint(drawPaints);
mDrawPath = new Path(drawPath);
}
Paint getDrawPaint() {
return mDrawPaint;
}
Path getDrawPath() {
return mDrawPath;
}
}
boolean undo() {
if (mLinePaths.size() > 0) {
mLinePaths.remove(mLinePaths.size() - 1);
invalidate();
}
return mLinePaths.size() != 0;
}
private void touchStart(float x, float y) {
mPath.reset();
mPath.moveTo(x, y);
mTouchX = x;
mTouchY = y;
}
private void touchMove(float x, float y) {
float dx = Math.abs(x - mTouchX);
float dy = Math.abs(y - mTouchY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mTouchX, mTouchY, (x + mTouchX) / 2, (y + mTouchY) / 2);
mTouchX = x;
mTouchY = y;
}
}
private void touchUp() {
mPath.lineTo(mTouchX, mTouchY);
// Commit the path to our offscreen
mDrawCanvas.drawPath(mPath, mDrawPaint);
// kill this so we don't double draw
mLinePaths.add(new LinePath(mPath, mDrawPaint));
mPath = new Path();
}
}
为了方便使用实现部分方法封装构造器PhotoEditor:
public class PhotoEditor {
private static final String TAG = PhotoEditor.class.getSimpleName();
private PhotoEditorView parentView;
private BrushDrawingView brushDrawingView;
private Context context;
private PhotoEditor(Builder builder) {
this.context = builder.context;
this.parentView = builder.parentView;
this.brushDrawingView = builder.brushDrawingView;
}
public void setBrushDrawingMode(boolean brushDrawingMode) {
if (brushDrawingView != null) {
brushDrawingView.setBrushDrawingMode(brushDrawingMode);
}
}
public Boolean getBrushDrawableMode() {
return brushDrawingView != null && brushDrawingView.getBrushDrawingMode();
}
public void setBrushSize(float size) {
if (brushDrawingView != null) {
brushDrawingView.setBrushSize(size);
}
}
public void setOpacity(@IntRange(from = 0, to = 100) int opacity) {
if (brushDrawingView != null) {
opacity = (int) ((opacity / 100.0) * 255.0);
brushDrawingView.setOpacity(opacity);
}
}
public void setPaintColor(@ColorInt int color) {
if (brushDrawingView != null) {
brushDrawingView.setBrushColor(color);
}
}
public boolean undo() {
return brushDrawingView != null && brushDrawingView.undo();
}
public void clearBrushAllViews() {
if (brushDrawingView != null) {
brushDrawingView.clearAll();
}
}
public interface OnSaveListener {
void onStart();
void onSuccess(String imagePath);
void onFailure(Boolean success);
}
@SuppressLint("StaticFieldLeak")
@RequiresPermission(allOf = {Manifest.permission.WRITE_EXTERNAL_STORAGE})
public void saveImage(@NonNull final String imagePath, @NonNull final OnSaveListener onSaveListener) {
new AsyncTask<String, String, Boolean>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
onSaveListener.onStart();
}
@SuppressLint("MissingPermission")
@Override
protected Boolean doInBackground(String... strings) {
// Create a media file name
File file = new File(imagePath);
try {
FileOutputStream out = new FileOutputStream(file, false);
if (parentView != null) {
Bitmap drawingCache = parentView.getResultBitmap();
drawingCache.compress(Bitmap.CompressFormat.JPEG, 100, out);
}
out.flush();
out.close();
return true;
} catch (Exception e) {
e.printStackTrace();
onSaveListener.onFailure(false);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
super.onPostExecute(success);
if (success) {
clearBrushAllViews();
onSaveListener.onSuccess(imagePath);
} else {
onSaveListener.onFailure(success);
}
}
}.execute();
}
/**
* Check if any changes made need to save
*
* @return true is nothing is there to change
*/
public boolean isCacheEmpty() {
if (brushDrawingView != null) {
return brushDrawingView.isCacheEmpty();
} else {
return true;
}
}
public static class Builder {
private Context context;
private PhotoEditorView parentView;
private BrushDrawingView brushDrawingView;
public Builder(Context context, PhotoEditorView photoEditorView) {
this.context = context;
parentView = photoEditorView;
brushDrawingView = photoEditorView.getBrushDrawingView();
}
public PhotoEditor build() {
return new PhotoEditor(this);
}
}
}
使用:
public class PhotoEditorActivity extends AppCompatActivity {
public static final String TYPE = "type";
public static final int TYPE_ALBUM = 1;
public static final int TYPE_OTHER = 2;
public static final String EXTRA_EDITOR_MEDIA = "extra_editor_media";
public static final String RESULT_EDITOR_MEDIA = "result_editor_media";
public static final String EXTRA_IMAGE_PATH = "extra_image_path";
public static final String RESULT_IMAGE_PATH = "result_image_path";
public static final int REQUESTCODE = 102;
public static final int RESULTCODE = 100;
private static String storagePath = "";
private static final File parentPath = Environment.getExternalStorageDirectory();
private static String EDITOR_PHOTO_NAME = "Editor";
private Context mContext;
private PhotoEditorView photoEditorView;
private TextView btnCancel;
private TextView btnComplete;
private ColorRadioGroup crgColors;
private ImageButton btnUndo;
private PhotoEditor photoEditor;
private String url;
private String savePath;
private ProgressDialog mProgressDialog;
private Intent mIntent;
private LocalMedia localMedial;
private int type;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.photo_editor_activity);
mContext = this;
btnUndo = findViewById(R.id.btn_undo);
crgColors = findViewById(R.id.crg_colors);
btnComplete = findViewById(R.id.btn_complete);
btnCancel = findViewById(R.id.btn_cancel);
photoEditorView = findViewById(R.id.photo_editor_view);
initWidget();
bindListener();
startInvoke();
}
public void initWidget() {
photoEditor = new PhotoEditor.Builder(this, photoEditorView).build();
photoEditor.setBrushDrawingMode(true);
crgColors.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
photoEditor.setPaintColor(crgColors.getCheckColor());
}
});
//编辑保存地址
savePath = saveEditorPhotoJpgPath();
}
private void bindListener() {
btnCancel.setOnClickListener(listener);
btnUndo.setOnClickListener(listener);
btnComplete.setOnClickListener(listener);
}
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
int i = v.getId();
if (i == R.id.btn_cancel) {
finish();
} else if (i == R.id.btn_undo) {
photoEditor.undo();
} else if (i == R.id.btn_complete) {
if (photoEditor.isCacheEmpty()) {
if (type == TYPE_OTHER) {
mIntent = new Intent();
mIntent.putExtra(RESULT_IMAGE_PATH, url);
setResult(RESULTCODE, mIntent);
}
finish();
} else {
saveImage();
}
}
}
};
public void startInvoke() {
mIntent = getIntent();
if (mIntent != null) {
type = mIntent.getIntExtra(TYPE, 0);
if (type == TYPE_ALBUM) {
localMedial = mIntent.getParcelableExtra(EXTRA_EDITOR_MEDIA);
if (localMedial != null) {
url = localMedial.getPath();
}
} else {
url = mIntent.getStringExtra(EXTRA_IMAGE_PATH);
}
if (!TextUtils.isEmpty(url)) {
Glide.with(this).asBitmap().load(url).into(photoEditorView.getImageView());
} else {
finish();
}
}
}
private String initPath() {
if (storagePath.equals("")) {
storagePath = parentPath.getAbsolutePath() + File.separator + EDITOR_PHOTO_NAME;
File file = new File(storagePath);
if (!file.exists()) {
file.mkdir();
}
}
return storagePath;
}
public String saveEditorPhotoJpgPath() {
return initPath() + File.separator + "editor_" + System.currentTimeMillis() + ".jpg";
}
@SuppressLint("MissingPermission")
private void saveImage() {
photoEditor.saveImage(savePath, new PhotoEditor.OnSaveListener() {
@Override
public void onStart() {
showLoading("正在处理中");
}
@Override
public void onSuccess(String imagePath) {
hideLoading();
mIntent = new Intent();
if (type == TYPE_ALBUM) {
localMedial.setEditor(true);
localMedial.setEditorPath(imagePath);
mIntent.putExtra(RESULT_EDITOR_MEDIA, localMedial);
} else {
mIntent.putExtra(RESULT_IMAGE_PATH, imagePath);
}
setResult(RESULTCODE, mIntent);
finish();
}
@Override
public void onFailure(Boolean success) {
hideLoading();
Toast.makeText(mContext, "失败", Toast.LENGTH_LONG).show();
}
});
}
void showLoading(@NonNull String message) {
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setMessage(message);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCancelable(false);
mProgressDialog.show();
}
void hideLoading() {
if (mProgressDialog != null) {
mProgressDialog.dismiss();
}
}
}
布局photo_editor_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_radio"
>
<com.luck.picture.lib.editor.PhotoEditorView
android:id="@+id/photo_editor_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentTop="true"
>
<TextView
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:gravity="center"
android:paddingEnd="@dimen/photo_editor_text_padding"
android:paddingStart="@dimen/photo_editor_text_padding"
android:text="取消"
android:textColor="@color/white_radio"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:gravity="center"
android:text="编辑图片"
android:textColor="@color/white_radio"/>
<TextView
android:id="@+id/btn_complete"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:gravity="center"
android:paddingEnd="@dimen/photo_editor_text_padding"
android:paddingStart="@dimen/photo_editor_text_padding"
android:text="完成"
android:textColor="@color/white_radio"
/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.luck.picture.lib.editor.ColorRadioGroup
android:id="@+id/crg_colors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="@dimen/radio_margin"
android:layout_weight="1"
android:checkedButton="@+id/rb_red"
android:orientation="horizontal">
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_black"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/black_radio"/>
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_red"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/red_radio"/>
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_yellow"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/yellow_radio"/>
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_blue"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/blue_radio"/>
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_green"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/green_radio"/>
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_orange"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/orange_radio"/>
<com.luck.picture.lib.editor.ColorRadioButton
android:id="@+id/rb_white"
android:layout_width="@dimen/radio_color"
android:layout_height="@dimen/radio_color"
android:layout_margin="@dimen/radio_color_margin"
android:button="@null"
app:radio_color="@color/white_radio"/>
</com.luck.picture.lib.editor.ColorRadioGroup>
<ImageButton
android:id="@+id/btn_undo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/radio_margin"
android:background="@null"
android:src="@drawable/selector_btn_undo"/>
</LinearLayout>
</RelativeLayout>
完整代码已经合并到这里写链接内容com.luck.picture.lib.editor目录下调用activity为com.luck.picture.lib.PhotoEditorActivity