├── .gitignore ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── ishow │ │ └── cutout │ │ ├── CutoutActivity.java │ │ ├── CutoutRecord.java │ │ └── CutoutView.java │ └── res │ ├── color │ └── normal_grey_selected_white.xml │ ├── drawable-xhdpi │ ├── imitate_transparent_bg_piece.png │ └── splash.png │ ├── layout │ └── activity_main.xml │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk imagePath, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # Intellij 38 | *.iml 39 | .idea/ 40 | 41 | # Keystore files 42 | *.jks 43 | 44 | # External native build folder generated in Android Studio 2.2 and later 45 | .externalNativeBuild 46 | 47 | # Google Services (e.g. APIs or Firebase) 48 | google-services.json 49 | 50 | # Freeline 51 | freeline.py 52 | freeline/ 53 | freeline_project_description.json 54 | 55 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "com.ishow.demo" 8 | minSdkVersion 17 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | 25 | 26 | compile 'com.ishow:common:1.1.2' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Environment\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include imagePath and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/ishow/cutout/CutoutActivity.java: -------------------------------------------------------------------------------- 1 | package com.ishow.cutout; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import com.ishow.common.app.activity.BaseActivity; 8 | import com.ishow.common.utils.image.loader.ImageLoader; 9 | import com.ishow.common.utils.image.select.OnSelectPhotoListener; 10 | import com.ishow.common.utils.image.select.SelectPhotoUtils; 11 | import com.ishow.common.widget.loading.LoadingDialog; 12 | 13 | import java.util.List; 14 | 15 | import static com.ishow.cutout.R.id.cutout; 16 | 17 | public class CutoutActivity extends BaseActivity implements OnSelectPhotoListener, View.OnClickListener { 18 | private SelectPhotoUtils mSelectPhotoUtils; 19 | private CutoutView mCutoutView; 20 | private LoadingDialog mLoadingDialog; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.activity_main); 26 | ImageLoader.init(this); 27 | mCutoutView = (CutoutView) findViewById(cutout); 28 | mCutoutView.setMode(CutoutView.Mode.CUT_OUT); 29 | mCutoutView.setOnCutoutListener(new CutoutView.OnCutoutListener() { 30 | @Override 31 | public void canBack(int count) { 32 | 33 | } 34 | 35 | @Override 36 | public void showLoading() { 37 | mLoadingDialog = LoadingDialog.show(CutoutActivity.this, mLoadingDialog); 38 | } 39 | 40 | @Override 41 | public void dismissLoading() { 42 | LoadingDialog.dismiss(mLoadingDialog); 43 | } 44 | }); 45 | mCutoutView.setPhoto("/storage/emulated/0/Android/data/com.ishow.demo/cache/5EC29A97-A8E4-4DAA-AFFC-B654F0B1436C.jpg"); 46 | 47 | mSelectPhotoUtils = new SelectPhotoUtils(this, SelectPhotoUtils.SelectMode.SINGLE); 48 | mSelectPhotoUtils.setOnSelectPhotoListener(this); 49 | 50 | 51 | View button = findViewById(R.id.button); 52 | button.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | mSelectPhotoUtils.select(); 56 | } 57 | }); 58 | 59 | View cut = findViewById(R.id.cut); 60 | cut.setOnClickListener(this); 61 | View back = findViewById(R.id.back); 62 | back.setOnClickListener(this); 63 | View eraser = findViewById(R.id.eraser); 64 | eraser.setOnClickListener(this); 65 | } 66 | 67 | @Override 68 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 69 | super.onActivityResult(requestCode, resultCode, data); 70 | mSelectPhotoUtils.onActivityResult(requestCode, resultCode, data); 71 | } 72 | 73 | @Override 74 | public void onSelectedPhoto(List multiPath, String singlePath) { 75 | mCutoutView.setPhoto(singlePath); 76 | } 77 | 78 | @Override 79 | public void onClick(View v) { 80 | switch (v.getId()) { 81 | case R.id.cut: 82 | mCutoutView.setMode(CutoutView.Mode.CUT_OUT); 83 | break; 84 | case R.id.back: 85 | mCutoutView.back(); 86 | break; 87 | case R.id.eraser: 88 | mCutoutView.setMode(CutoutView.Mode.ERASER); 89 | break; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/ishow/cutout/CutoutRecord.java: -------------------------------------------------------------------------------- 1 | package com.ishow.cutout; 2 | 3 | import android.graphics.Matrix; 4 | import android.graphics.Path; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * Created by yuhaiyang on 2017/9/28. 11 | * 历史记录 12 | */ 13 | 14 | @SuppressWarnings("ALL") 15 | public class CutoutRecord { 16 | /** 17 | * 图片路径 18 | */ 19 | private String imagePath; 20 | /** 21 | * 擦图 22 | */ 23 | private List eraserPathList; 24 | private List eraserMatrixList; 25 | 26 | 27 | /** 28 | * 抠图的路径 29 | */ 30 | private List cutoutPathList; 31 | private List cutoutMatrixList; 32 | /** 33 | * 抠图的轨迹 34 | */ 35 | private List cutoutTrackList; 36 | 37 | public CutoutRecord() { 38 | } 39 | 40 | public String getImagePath() { 41 | return imagePath; 42 | } 43 | 44 | public void setImagePath(String imagePath) { 45 | this.imagePath = imagePath; 46 | } 47 | 48 | 49 | public void addEraserMatrix(Matrix matrix) { 50 | if (matrix == null) { 51 | return; 52 | } 53 | if (eraserMatrixList == null) { 54 | eraserMatrixList = new ArrayList<>(); 55 | } 56 | eraserMatrixList.add(matrix); 57 | } 58 | 59 | public void addEraserPath(Path path) { 60 | if (path == null) { 61 | return; 62 | } 63 | if (eraserPathList == null) { 64 | eraserPathList = new ArrayList<>(); 65 | } 66 | eraserPathList.add(path); 67 | } 68 | 69 | 70 | public void addCutoutMatrix(Matrix matrix) { 71 | if (matrix == null) { 72 | return; 73 | } 74 | if (cutoutMatrixList == null) { 75 | cutoutMatrixList = new ArrayList<>(); 76 | } 77 | cutoutMatrixList.add(matrix); 78 | } 79 | 80 | public void addCutoutPath(Path path) { 81 | if (path == null) { 82 | return; 83 | } 84 | if (cutoutPathList == null) { 85 | cutoutPathList = new ArrayList<>(); 86 | } 87 | cutoutPathList.add(path); 88 | } 89 | 90 | public List getCutoutMatrixList() { 91 | if (cutoutMatrixList == null) { 92 | cutoutMatrixList = new ArrayList<>(); 93 | } 94 | return cutoutMatrixList; 95 | } 96 | 97 | public List getCutoutPathList() { 98 | if (cutoutPathList == null) { 99 | cutoutPathList = new ArrayList<>(); 100 | } 101 | return cutoutPathList; 102 | } 103 | 104 | public void addCutoutTrack(float length) { 105 | if (cutoutTrackList == null) { 106 | cutoutTrackList = new ArrayList<>(); 107 | } 108 | cutoutTrackList.add(length); 109 | } 110 | 111 | 112 | public List getEraserPathList() { 113 | if (eraserPathList == null) { 114 | eraserPathList = new ArrayList<>(); 115 | } 116 | return eraserPathList; 117 | } 118 | 119 | public List getEraserMatrixList() { 120 | if (eraserMatrixList == null) { 121 | eraserMatrixList = new ArrayList<>(); 122 | } 123 | return eraserMatrixList; 124 | } 125 | 126 | public List getCutoutTrackList() { 127 | if (cutoutTrackList == null) { 128 | cutoutTrackList = new ArrayList<>(); 129 | } 130 | return cutoutTrackList; 131 | } 132 | 133 | public void clearPointList() { 134 | if (cutoutTrackList == null) { 135 | return; 136 | } 137 | cutoutTrackList.clear(); 138 | } 139 | 140 | public void clearEraserInfo() { 141 | clearEraserMatrixList(); 142 | clearEraserPathList(); 143 | } 144 | 145 | public void clearEraserMatrixList() { 146 | getEraserMatrixList().clear(); 147 | } 148 | 149 | public void clearEraserPathList() { 150 | getEraserPathList().clear(); 151 | } 152 | 153 | public void clearCutout() { 154 | getCutoutTrackList().clear(); 155 | getCutoutMatrixList().clear(); 156 | getCutoutPathList().clear(); 157 | } 158 | 159 | /** 160 | * 是否有 记录 161 | */ 162 | public boolean hasRecord() { 163 | return !getEraserPathList().isEmpty() || !getCutoutTrackList().isEmpty(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/com/ishow/cutout/CutoutView.java: -------------------------------------------------------------------------------- 1 | package com.ishow.cutout; 2 | 3 | import android.animation.PropertyValuesHolder; 4 | import android.animation.ValueAnimator; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.graphics.BitmapShader; 9 | import android.graphics.BlurMaskFilter; 10 | import android.graphics.Canvas; 11 | import android.graphics.Color; 12 | import android.graphics.Matrix; 13 | import android.graphics.Paint; 14 | import android.graphics.Path; 15 | import android.graphics.PathMeasure; 16 | import android.graphics.PorterDuff; 17 | import android.graphics.PorterDuffXfermode; 18 | import android.graphics.RectF; 19 | import android.graphics.Shader; 20 | import android.os.Environment; 21 | import android.support.annotation.IntDef; 22 | import android.support.annotation.Nullable; 23 | import android.util.AttributeSet; 24 | import android.util.Log; 25 | import android.view.MotionEvent; 26 | import android.view.View; 27 | import android.view.ViewConfiguration; 28 | import android.widget.Toast; 29 | 30 | import com.ishow.common.utils.StringUtils; 31 | import com.ishow.common.utils.image.ImageUtils; 32 | 33 | import java.io.File; 34 | import java.io.FileOutputStream; 35 | import java.lang.annotation.Retention; 36 | import java.lang.annotation.RetentionPolicy; 37 | import java.util.ArrayList; 38 | import java.util.List; 39 | import java.util.UUID; 40 | 41 | 42 | /** 43 | * Created by yuhaiyang on 2017/9/25. 44 | * 抠图 45 | */ 46 | public class CutoutView extends View { 47 | 48 | private static final String TAG = "CutoutView"; 49 | 50 | /** 51 | * Donw和up之间的有效距离 52 | * 单位px 53 | */ 54 | private static final int EFFECTIVE_DISTANCE = 200; 55 | /** 56 | * 移动的有效距离 57 | */ 58 | private static final int EFFECTIVE_MOVE_DISTANCE = 500; 59 | /** 60 | * 抠图画笔的透明度 61 | */ 62 | private static final int TRACK_ALPHA = 100; 63 | 64 | /** 65 | * 最大放大倍数 66 | */ 67 | private static final float MAX_ZOOM_SCALE = 2.5F; 68 | /** 69 | * 缩放的最小倍数 70 | */ 71 | private static final float MIN_ZOOM_SCALE = 0.3F; 72 | 73 | /** 74 | * 动作路径 75 | */ 76 | private Path mCurrentPath; 77 | private Path mRealPath; 78 | /** 79 | * 用来计算动作路径 80 | */ 81 | private PathMeasure mPathMeasure; 82 | 83 | private Paint mPhotoPaint; 84 | private Paint mActionPaint; 85 | private Paint mEnlargePaint; 86 | private Paint mTransparentPaint; 87 | 88 | private PorterDuffXfermode mCutoutPorterMode; 89 | private PorterDuffXfermode mEnlargePorterMode; 90 | private PorterDuffXfermode mEraserPorterMode; 91 | private Bitmap mPhotoBitmap; 92 | private Bitmap mCutoutBitmap; 93 | private Bitmap mEnlargeBgBitmap; 94 | 95 | private boolean isActionTrackVisible; 96 | private boolean isEnlargeVisible; 97 | private boolean isCutoutNewPath; 98 | /** 99 | * 是否手势操作过 100 | */ 101 | private boolean isGestured; 102 | private boolean isMoved; 103 | /** 104 | * 模式 105 | * 抠图模式 {@link Mode#CUT_OUT} 106 | * 擦图模式 {@link Mode#ERASER} 107 | */ 108 | private int mMode; 109 | 110 | private int mPhotoTop; 111 | private int mPhotoLeft; 112 | private int mPhotoWidth; 113 | private int mPhotoHeight; 114 | private int mViewWidth; 115 | private int mViewHeight; 116 | /** 117 | * 放大视图 118 | */ 119 | private int mEnlargeSize; 120 | private int mMinDistance; 121 | /** 122 | * 多点触控处理 123 | */ 124 | private int mTouchPoint; 125 | private float[] mDownPoint; 126 | private float[] mMovePoint; 127 | private float[] mUpPoint; 128 | private float[] mTouchTwoPointCenter; 129 | private float[] mLastPointOne; 130 | private float[] mLastPointTwo; 131 | private float[] mStartCutoutPoint; 132 | private float[] mLastCutoutPoint; 133 | 134 | /** 135 | * 上一次2点之间的距离 136 | */ 137 | private double mLastTwoPointDistance; 138 | 139 | 140 | private List mCutoutRecordList; 141 | private CutoutRecord mCurrentRecord; 142 | private ValueAnimator mZoomAnimator; 143 | 144 | private Matrix mMatrix; 145 | private Matrix mExchangedMatrix; 146 | private RectF mPhotoRectF; 147 | 148 | public CutoutView(Context context) { 149 | super(context); 150 | init(context); 151 | } 152 | 153 | public CutoutView(Context context, @Nullable AttributeSet attrs) { 154 | super(context, attrs); 155 | init(context); 156 | } 157 | 158 | public CutoutView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 159 | super(context, attrs, defStyleAttr); 160 | init(context); 161 | } 162 | 163 | private void init(Context context) { 164 | setLayerType(View.LAYER_TYPE_SOFTWARE, null); 165 | mMinDistance = ViewConfiguration.get(context).getScaledTouchSlop(); 166 | mMatrix = new Matrix(); 167 | mExchangedMatrix = new Matrix(); 168 | mPhotoRectF = new RectF(); 169 | 170 | mMode = Mode.CUT_OUT; 171 | 172 | mCutoutRecordList = new ArrayList<>(); 173 | mDownPoint = new float[2]; 174 | mMovePoint = new float[2]; 175 | mUpPoint = new float[2]; 176 | mTouchTwoPointCenter = new float[2]; 177 | mLastPointOne = new float[2]; 178 | mLastPointTwo = new float[2]; 179 | mStartCutoutPoint = new float[2]; 180 | mLastCutoutPoint = new float[2]; 181 | 182 | mPhotoPaint = new Paint(); 183 | mPhotoPaint.setDither(true); 184 | mPhotoPaint.setAntiAlias(true); 185 | mPhotoPaint.setStyle(Paint.Style.FILL); 186 | 187 | mZoomAnimator = ValueAnimator.ofFloat(); 188 | mZoomAnimator.setDuration(500); 189 | mZoomAnimator.addUpdateListener(mAniListener); 190 | 191 | 192 | // 透明背景 193 | Bitmap transparentBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.imitate_transparent_bg_piece); 194 | BitmapShader transparentShader = new BitmapShader(transparentBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); 195 | mTransparentPaint = new Paint(); 196 | mTransparentPaint.setDither(true); 197 | mTransparentPaint.setAntiAlias(true); 198 | mTransparentPaint.setShader(transparentShader); 199 | 200 | mCurrentPath = new Path(); 201 | mRealPath = new Path(); 202 | 203 | 204 | mActionPaint = new Paint(); 205 | mActionPaint.setDither(true); 206 | mActionPaint.setAntiAlias(true); 207 | mActionPaint.setStyle(Paint.Style.STROKE); 208 | mActionPaint.setColor(Color.RED); 209 | mActionPaint.setStrokeWidth(30); 210 | mActionPaint.setStrokeCap(Paint.Cap.ROUND); 211 | mActionPaint.setStrokeJoin(Paint.Join.ROUND); 212 | 213 | mEnlargePaint = new Paint(); 214 | mEnlargePaint.setDither(true); 215 | mEnlargePaint.setAntiAlias(true); 216 | mEnlargePaint.setStyle(Paint.Style.STROKE); 217 | mEnlargePaint.setColor(Color.WHITE); 218 | mEnlargePaint.setStrokeWidth(5); 219 | mEnlargePaint.setStrokeCap(Paint.Cap.ROUND); 220 | mEnlargePaint.setStrokeJoin(Paint.Join.ROUND); 221 | 222 | mPathMeasure = new PathMeasure(mCurrentPath, false); 223 | mCutoutPorterMode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 224 | mEnlargePorterMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); 225 | mEraserPorterMode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); 226 | } 227 | 228 | 229 | @Override 230 | public boolean onTouchEvent(MotionEvent event) { 231 | switch (event.getAction() & MotionEvent.ACTION_MASK) { 232 | case MotionEvent.ACTION_DOWN: 233 | mTouchPoint = 1; 234 | isEnlargeVisible = true; 235 | isGestured = false; 236 | isMoved = false; 237 | mDownPoint[0] = event.getX(); 238 | mDownPoint[1] = event.getY(); 239 | break; 240 | case MotionEvent.ACTION_POINTER_DOWN: 241 | mTouchPoint += 1; 242 | isEnlargeVisible = false; 243 | isGestured = true; 244 | 245 | mLastPointOne[0] = event.getX(0); 246 | mLastPointOne[1] = event.getY(0); 247 | mLastPointTwo[0] = event.getX(1); 248 | mLastPointTwo[1] = event.getY(1); 249 | mLastTwoPointDistance = getTouchPointDistance(mLastPointOne, mLastPointTwo); 250 | break; 251 | case MotionEvent.ACTION_MOVE: 252 | mMovePoint[0] = event.getX(); 253 | mMovePoint[1] = event.getY(); 254 | 255 | final int moveX = (int) Math.abs(mMovePoint[0] - mDownPoint[0]); 256 | final int moveY = (int) Math.abs(mMovePoint[1] - mDownPoint[1]); 257 | 258 | if (moveX < mMinDistance && moveY < mMinDistance) { 259 | return true; 260 | } 261 | 262 | 263 | if (mTouchPoint >= 2) { 264 | onGestureMove(event); 265 | } else if (!isGestured) { // 手势操作后不能进行其他操作 266 | if (!isMoved) { 267 | if (mMode == Mode.CUT_OUT) { 268 | onCutoutDown(event); 269 | } else { 270 | onEraserDown(event); 271 | } 272 | isMoved = true; 273 | } else { 274 | if (mMode == Mode.CUT_OUT) { 275 | onCutoutMove(event); 276 | } else { 277 | onEraserMove(event); 278 | } 279 | } 280 | 281 | 282 | } 283 | 284 | break; 285 | case MotionEvent.ACTION_POINTER_UP: 286 | mTouchPoint -= 1; 287 | if (mTouchPoint <= 1) { 288 | onGestureUp(event); 289 | } 290 | break; 291 | 292 | case MotionEvent.ACTION_UP: 293 | mTouchPoint = 0; 294 | isEnlargeVisible = false; 295 | mUpPoint[0] = event.getX(); 296 | mUpPoint[1] = event.getY(); 297 | // 手势操作后不能进行其他操作 298 | if (!isGestured) { 299 | if (mMode == Mode.CUT_OUT) { 300 | onCutoutUp(event); 301 | } else { 302 | onEraserUp(event); 303 | } 304 | } 305 | break; 306 | 307 | } 308 | postInvalidate(); 309 | return true; 310 | } 311 | 312 | @Override 313 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 314 | super.onSizeChanged(w, h, oldw, oldh); 315 | 316 | 317 | mViewWidth = w; 318 | mViewHeight = h; 319 | 320 | mEnlargeSize = w / 4; 321 | createEnlargeBgBitmap(); 322 | computePhotoInfo(); 323 | } 324 | 325 | 326 | @Override 327 | protected void onDraw(Canvas canvas) { 328 | super.onDraw(canvas); 329 | if (mPhotoBitmap == null || mPhotoBitmap.isRecycled()) { 330 | return; 331 | } 332 | canvas.save(); 333 | canvas.concat(mMatrix); 334 | /* 335 | * 画仿透明的背景 336 | */ 337 | canvas.drawRect( 338 | mPhotoLeft, 339 | mPhotoTop, 340 | mPhotoLeft + mPhotoBitmap.getWidth(), 341 | mPhotoTop + mPhotoBitmap.getHeight(), 342 | mTransparentPaint); 343 | 344 | canvas.drawBitmap(mPhotoBitmap, mPhotoLeft, mPhotoTop, mPhotoPaint); 345 | 346 | if (isActionTrackVisible) { 347 | canvas.drawBitmap(mCutoutBitmap, mPhotoLeft, mPhotoTop, mPhotoPaint); 348 | } 349 | canvas.restore(); 350 | 351 | if (isActionTrackVisible && !isCutoutNewPath) { 352 | canvas.drawPath(mCurrentPath, mActionPaint); 353 | } 354 | 355 | drawEnlarge(canvas); 356 | } 357 | 358 | private void drawEnlarge(Canvas canvas) { 359 | if (!isEnlargeVisible || mEnlargeBgBitmap == null) { 360 | return; 361 | } 362 | final int left; 363 | final float effect = mEnlargeSize * 1.2f; 364 | if (mMovePoint[0] < effect && mMovePoint[1] < effect) { 365 | left = getMeasuredWidth() - mEnlargeSize; 366 | } else { 367 | left = 0; 368 | } 369 | 370 | float[] movePoint = new float[2]; 371 | mExchangedMatrix.reset(); 372 | mMatrix.invert(mExchangedMatrix); 373 | mExchangedMatrix.mapPoints(movePoint, mMovePoint); 374 | 375 | final int x = left + mEnlargeSize / 2; 376 | final int y = mEnlargeSize / 2; 377 | final float moveX = x - movePoint[0] + mPhotoLeft; 378 | final float moveY = y - movePoint[1] + mPhotoTop; 379 | 380 | canvas.drawRect( 381 | left, 382 | 0, 383 | left + mEnlargeSize, 384 | mEnlargeSize, 385 | mTransparentPaint); 386 | 387 | 388 | Bitmap bitmap = getEnlargeResultBitmap(); 389 | int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG); 390 | mPhotoPaint.setXfermode(null); 391 | canvas.drawBitmap(mEnlargeBgBitmap, left, 0, mPhotoPaint); 392 | mPhotoPaint.setXfermode(mEnlargePorterMode); 393 | canvas.drawBitmap(bitmap, moveX, moveY, mPhotoPaint); 394 | mPhotoPaint.setXfermode(null); 395 | canvas.restoreToCount(saveCount); 396 | 397 | canvas.drawRect(left, 0, left + mEnlargeSize, mEnlargeSize, mEnlargePaint); 398 | canvas.drawCircle(x, y, 15, mEnlargePaint); 399 | 400 | recycleBitmap(bitmap); 401 | 402 | } 403 | 404 | /** 405 | * 抠图按下操作 406 | */ 407 | @SuppressWarnings("UnusedParameters") 408 | private void onCutoutDown(MotionEvent event) { 409 | isActionTrackVisible = true; 410 | isCutoutNewPath = false; 411 | mActionPaint.setAlpha(TRACK_ALPHA); 412 | 413 | float[] real = computeRealPoint(mDownPoint); 414 | 415 | mPathMeasure.setPath(mRealPath, false); 416 | // 如果之前是closed 就重置 417 | if (mPathMeasure.getLength() == 0 || mPathMeasure.isClosed()) { 418 | mRealPath.reset(); 419 | mRealPath.moveTo(real[0], real[1]); 420 | mCurrentPath.reset(); 421 | mCurrentPath.moveTo(mDownPoint[0], mDownPoint[1]); 422 | mStartCutoutPoint = real; 423 | } else { 424 | mRealPath.lineTo(real[0], real[1]); 425 | 426 | mLastCutoutPoint = computeNowPoint(mLastCutoutPoint); 427 | mCurrentPath.moveTo(mLastCutoutPoint[0], mLastCutoutPoint[1]); 428 | mCurrentPath.lineTo(mDownPoint[0], mDownPoint[1]); 429 | } 430 | 431 | } 432 | 433 | private void onCutoutMove(MotionEvent event) { 434 | float[] real = computeRealPoint(mMovePoint); 435 | mRealPath.lineTo(real[0], real[1]); 436 | mCurrentPath.lineTo(event.getX(), event.getY()); 437 | } 438 | 439 | private void onCutoutUp(MotionEvent event) { 440 | isCutoutNewPath = true; 441 | float[] real = computeRealPoint(mUpPoint); 442 | mRealPath.lineTo(real[0], real[1]); 443 | mCurrentPath.lineTo(event.getX(), event.getY()); 444 | 445 | mPathMeasure.setPath(mRealPath, false); 446 | final float actionLength = mPathMeasure.getLength(); 447 | final double distance = getPointDistance(mStartCutoutPoint, real); 448 | if (actionLength > EFFECTIVE_MOVE_DISTANCE && distance < EFFECTIVE_DISTANCE) { 449 | mRealPath.close(); 450 | final float start[] = computeNowPoint(mStartCutoutPoint); 451 | Path path = new Path(); 452 | path.moveTo(event.getX(), event.getY()); 453 | path.lineTo(start[0], start[1]); 454 | updateCutoutPathBitmap(path); 455 | 456 | new Thread(mResultCutoutRunnable).start(); 457 | } else { 458 | mCurrentRecord.addCutoutTrack(mPathMeasure.getLength()); 459 | updateCutoutPathBitmap(); 460 | 461 | Matrix matrix = new Matrix(); 462 | mMatrix.invert(matrix); 463 | mCurrentRecord.addCutoutMatrix(matrix); 464 | mCurrentRecord.addCutoutPath(mCurrentPath); 465 | mCurrentPath = new Path(); 466 | 467 | mLastCutoutPoint[0] = event.getX(); 468 | mLastCutoutPoint[1] = event.getY(); 469 | mLastCutoutPoint = computeRealPoint(mLastCutoutPoint); 470 | 471 | notifyCanBack(); 472 | } 473 | } 474 | 475 | /** 476 | * 橡皮擦按下操作 477 | */ 478 | private void onEraserDown(MotionEvent event) { 479 | isActionTrackVisible = false; 480 | mActionPaint.setAlpha(255); 481 | mCurrentPath.reset(); 482 | mCurrentPath.moveTo(event.getX(), event.getY()); 483 | } 484 | 485 | private void onEraserMove(MotionEvent event) { 486 | mCurrentPath.lineTo(event.getX(), event.getY()); 487 | mPhotoBitmap = getEraserResultBitmap(); 488 | } 489 | 490 | @SuppressWarnings("UnusedParameters") 491 | private void onEraserUp(MotionEvent event) { 492 | Matrix matrix = new Matrix(); 493 | mMatrix.invert(matrix); 494 | mCurrentRecord.addEraserMatrix(matrix); 495 | mCurrentRecord.addEraserPath(mCurrentPath); 496 | mCurrentPath = new Path(); 497 | notifyCanBack(); 498 | } 499 | 500 | private void onGestureMove(MotionEvent event) { 501 | final float pointOneX = event.getX(0); 502 | final float pointOneY = event.getY(0); 503 | final float pointTwoX = event.getX(1); 504 | final float pointTwoY = event.getY(1); 505 | 506 | final float translateX = Math.min(pointOneX - mLastPointOne[0], pointTwoX - mLastPointTwo[0]); 507 | final float translateY = Math.min(pointOneY - mLastPointOne[1], pointTwoY - mLastPointTwo[1]); 508 | mMatrix.postTranslate(translateX, translateY); 509 | mLastPointOne[0] = pointOneX; 510 | mLastPointOne[1] = pointOneY; 511 | mLastPointTwo[0] = pointTwoX; 512 | mLastPointTwo[1] = pointTwoY; 513 | 514 | mTouchTwoPointCenter[0] = pointOneX + (pointTwoX - pointOneX) / 2; 515 | mTouchTwoPointCenter[1] = pointOneY + (pointTwoY - pointOneY) / 2; 516 | 517 | final float currentScale = getValues(Matrix.MSCALE_X); 518 | final double nowDistance = getTouchPointDistance(mLastPointOne, mLastPointTwo); 519 | float nowScale = (float) (nowDistance / mLastTwoPointDistance); 520 | mLastTwoPointDistance = nowDistance; 521 | 522 | mExchangedMatrix.set(mMatrix); 523 | mExchangedMatrix.postScale(nowScale, nowScale, mTouchTwoPointCenter[0], mTouchTwoPointCenter[1]); 524 | final float preScale = getValues(mExchangedMatrix, Matrix.MSCALE_X); 525 | if (preScale < MIN_ZOOM_SCALE) { 526 | nowScale = MIN_ZOOM_SCALE / currentScale; 527 | } else if (preScale > MAX_ZOOM_SCALE) { 528 | nowScale = MAX_ZOOM_SCALE / currentScale; 529 | } 530 | 531 | mMatrix.postScale(nowScale, nowScale, mTouchTwoPointCenter[0], mTouchTwoPointCenter[1]); 532 | } 533 | 534 | 535 | @SuppressWarnings("UnusedParameters") 536 | private void onGestureUp(MotionEvent event) { 537 | isGestured = true; 538 | final float scale = getValues(Matrix.MSCALE_X); 539 | 540 | if (scale > 1) { 541 | onGestureZoomIn(event, scale); 542 | } else { 543 | onGestureZoomOut(event, scale); 544 | } 545 | 546 | } 547 | 548 | 549 | /** 550 | * 放大 551 | */ 552 | @SuppressWarnings("UnusedParameters") 553 | private void onGestureZoomIn(MotionEvent event, float scale) { 554 | final float translateX = getValues(Matrix.MTRANS_X); 555 | final float translateY = getValues(Matrix.MTRANS_Y); 556 | 557 | resetPhotoRectF(); 558 | PropertyValuesHolder translateXValues = null; 559 | PropertyValuesHolder translateYValues = null; 560 | 561 | 562 | mMatrix.mapRect(mPhotoRectF); 563 | 564 | final float photoWidth = mPhotoRectF.width(); 565 | final float photoHeight = mPhotoRectF.height(); 566 | // X轴 567 | if (photoWidth > mViewWidth) { 568 | // 左移 569 | if (mPhotoRectF.right < mViewWidth) { 570 | final float result = translateX + (mViewWidth - mPhotoRectF.right); 571 | translateXValues = PropertyValuesHolder.ofFloat("translateX", translateX, result); 572 | } else if (mPhotoRectF.left > 0) { 573 | final float result = translateX - mPhotoRectF.left; 574 | translateXValues = PropertyValuesHolder.ofFloat("translateX", translateX, result); 575 | } 576 | } else { 577 | /* 578 | * 1. (mViewWidth - viewWidth) / 2 单侧移动的了多少距离 579 | * 2. (mPhotoRectF.left - (mViewWidth - viewWidth) / 2 需要移动的距离 580 | * 3. mTranslateX - (mPhotoRectF.left - (mViewWidth - viewWidth) / 2) 最终的距离 581 | */ 582 | final float result = translateX - (mPhotoRectF.left - (mViewWidth - photoWidth) / 2); 583 | translateXValues = PropertyValuesHolder.ofFloat("translateX", translateX, result); 584 | } 585 | 586 | // Y轴 587 | if (photoHeight > mViewHeight) { 588 | // 上移 589 | if (mPhotoRectF.top > 0) { 590 | final float result = translateY - mPhotoRectF.top; 591 | translateYValues = PropertyValuesHolder.ofFloat("translateY", translateY, result); 592 | } else if (mPhotoRectF.bottom < mViewHeight) { 593 | final float result = translateY + (mViewHeight - mPhotoRectF.bottom); 594 | translateYValues = PropertyValuesHolder.ofFloat("translateY", translateY, result); 595 | } 596 | } else { 597 | /* 598 | * 同X 599 | */ 600 | final float result = translateY - (mPhotoRectF.top - (mViewHeight - photoHeight) / 2); 601 | translateYValues = PropertyValuesHolder.ofFloat("translateY", translateY, result); 602 | } 603 | 604 | startAnimation(null, translateXValues, translateYValues); 605 | } 606 | 607 | /** 608 | * 缩小 609 | */ 610 | @SuppressWarnings("UnusedParameters") 611 | private void onGestureZoomOut(MotionEvent event, float scale) { 612 | final float translateX = getValues(Matrix.MTRANS_X); 613 | final float translateY = getValues(Matrix.MTRANS_Y); 614 | 615 | PropertyValuesHolder scaleValues = PropertyValuesHolder.ofFloat("scale", scale, 1); 616 | PropertyValuesHolder translateXValues = PropertyValuesHolder.ofFloat("translateX", translateX, 0); 617 | PropertyValuesHolder translateYValues = PropertyValuesHolder.ofFloat("translateY", translateY, 0); 618 | mTouchTwoPointCenter[0] = 0; 619 | mTouchTwoPointCenter[1] = 0; 620 | 621 | startAnimation(scaleValues, translateXValues, translateYValues); 622 | } 623 | 624 | private void startAnimation(PropertyValuesHolder scale, PropertyValuesHolder translateX, PropertyValuesHolder translateY) { 625 | if (mZoomAnimator != null && mZoomAnimator.isRunning()) { 626 | mZoomAnimator.cancel(); 627 | } 628 | 629 | List list = new ArrayList<>(); 630 | if (scale != null) { 631 | list.add(scale); 632 | } 633 | 634 | if (translateX != null) { 635 | list.add(translateX); 636 | } 637 | 638 | if (translateY != null) { 639 | list.add(translateY); 640 | } 641 | 642 | if (list.isEmpty()) { 643 | Log.i(TAG, "startAnimation: no animation"); 644 | return; 645 | } 646 | mZoomAnimator.setValues((PropertyValuesHolder[]) list.toArray(new PropertyValuesHolder[list.size()])); 647 | mZoomAnimator.start(); 648 | } 649 | 650 | 651 | private Bitmap getEnlargeResultBitmap() { 652 | Bitmap bitmap = Bitmap.createBitmap(mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), Bitmap.Config.ARGB_8888); 653 | Canvas canvas = new Canvas(bitmap); 654 | canvas.drawBitmap(mPhotoBitmap, 0, 0, mPhotoPaint); 655 | if (mMode == Mode.CUT_OUT) { 656 | mExchangedMatrix.reset(); 657 | mMatrix.invert(mExchangedMatrix); 658 | int saveCount = canvas.saveLayer(0, 0, mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG); 659 | canvas.translate(-mPhotoLeft, -mPhotoTop); 660 | canvas.concat(mExchangedMatrix); 661 | canvas.drawPath(mCurrentPath, mActionPaint); 662 | canvas.restoreToCount(saveCount); 663 | } 664 | return bitmap; 665 | } 666 | 667 | 668 | /** 669 | * 获取路径Bitmap 670 | */ 671 | private Bitmap getPathBitmap() { 672 | Bitmap bitmap = Bitmap.createBitmap(mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), Bitmap.Config.ARGB_8888); 673 | Canvas canvas = new Canvas(bitmap); 674 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 675 | paint.setMaskFilter(new BlurMaskFilter(100, BlurMaskFilter.Blur.NORMAL)); 676 | canvas.translate(-mPhotoLeft, -mPhotoTop); 677 | canvas.drawPath(mRealPath, paint); 678 | return bitmap; 679 | } 680 | 681 | 682 | private Bitmap getEraserResultBitmap() { 683 | Bitmap bitmap = Bitmap.createBitmap(mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), Bitmap.Config.ARGB_8888); 684 | Canvas canvas = new Canvas(bitmap); 685 | mPhotoPaint.setXfermode(null); 686 | canvas.drawBitmap(mPhotoBitmap, 0, 0, mPhotoPaint); 687 | mPhotoPaint.setXfermode(mEraserPorterMode); 688 | canvas.drawBitmap(getEraserPathBitmap(), -mPhotoLeft, -mPhotoTop, mPhotoPaint); 689 | mPhotoPaint.setXfermode(null); 690 | return bitmap; 691 | } 692 | 693 | private Bitmap getEraserPathBitmap() { 694 | mExchangedMatrix.reset(); 695 | mMatrix.invert(mExchangedMatrix); 696 | Bitmap bitmap = Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.ARGB_8888); 697 | Canvas canvas = new Canvas(bitmap); 698 | canvas.save(); 699 | canvas.concat(mExchangedMatrix); 700 | canvas.drawPath(mCurrentPath, mActionPaint); 701 | canvas.restore(); 702 | return bitmap; 703 | } 704 | 705 | 706 | private void updateCutoutPathBitmap() { 707 | mExchangedMatrix.reset(); 708 | mMatrix.invert(mExchangedMatrix); 709 | Canvas canvas = new Canvas(mCutoutBitmap); 710 | canvas.save(); 711 | canvas.translate(-mPhotoLeft, -mPhotoTop); 712 | canvas.concat(mExchangedMatrix); 713 | canvas.drawPath(mCurrentPath, mActionPaint); 714 | canvas.restore(); 715 | } 716 | 717 | private void updateCutoutPathBitmap(Path path) { 718 | mExchangedMatrix.reset(); 719 | mMatrix.invert(mExchangedMatrix); 720 | Canvas canvas = new Canvas(mCutoutBitmap); 721 | canvas.save(); 722 | canvas.translate(-mPhotoLeft, -mPhotoTop); 723 | canvas.concat(mExchangedMatrix); 724 | canvas.drawPath(mCurrentPath, mActionPaint); 725 | canvas.drawPath(path, mActionPaint); 726 | canvas.restore(); 727 | } 728 | 729 | private Bitmap getBackEraserPathBitmap(List pathList, List matrixList) { 730 | Bitmap bitmap = Bitmap.createBitmap(mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), Bitmap.Config.ARGB_8888); 731 | Canvas canvas = new Canvas(bitmap); 732 | for (int i = 0; i < pathList.size(); i++) { 733 | Path path = pathList.get(i); 734 | Matrix matrix = matrixList.get(i); 735 | canvas.save(); 736 | canvas.translate(-mPhotoLeft, -mPhotoTop); 737 | canvas.concat(matrix); 738 | canvas.drawPath(path, mActionPaint); 739 | canvas.restore(); 740 | } 741 | return bitmap; 742 | } 743 | 744 | 745 | private void createEnlargeBgBitmap() { 746 | if (mEnlargeBgBitmap != null) { 747 | mEnlargeBgBitmap.recycle(); 748 | mEnlargeBgBitmap = null; 749 | } 750 | Bitmap bitmap = Bitmap.createBitmap(mEnlargeSize, mEnlargeSize, Bitmap.Config.RGB_565); 751 | Canvas canvas = new Canvas(bitmap); 752 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 753 | canvas.drawRect( 754 | 0, 755 | 0, 756 | mEnlargeSize, 757 | mEnlargeSize, 758 | paint); 759 | 760 | mEnlargeBgBitmap = bitmap; 761 | } 762 | 763 | /** 764 | * 获取2点距离 765 | */ 766 | private double getPointDistance(float[] down, float[] up) { 767 | final double x = Math.pow(up[0] - down[0], 2); 768 | final double y = Math.pow(up[1] - down[1], 2); 769 | return Math.sqrt(x + y); 770 | } 771 | 772 | /** 773 | * 获取 距离 774 | */ 775 | private double getTouchPointDistance(float[] one, float[] two) { 776 | float x = one[0] - two[0]; 777 | float y = one[1] - two[1]; 778 | return Math.sqrt(x * x + y * y); 779 | } 780 | 781 | 782 | /** 783 | * 计算图片相关信息 784 | */ 785 | private void computePhotoInfo() { 786 | // 当是第二张的时候不需要处理 787 | if (mCutoutRecordList.size() > 1) { 788 | return; 789 | } 790 | 791 | if (mPhotoBitmap == null || mViewHeight == 0 || mViewWidth == 0) { 792 | return; 793 | } 794 | 795 | final int width = mPhotoBitmap.getWidth(); 796 | final int height = mPhotoBitmap.getHeight(); 797 | final float widthScale = ((float) mViewWidth / width); 798 | final float heightScale = ((float) mViewHeight / height); 799 | 800 | Matrix matrix = new Matrix(); 801 | final float scale = Math.min(widthScale, heightScale); 802 | if (widthScale <= heightScale) { 803 | mPhotoLeft = 0; 804 | mPhotoTop = (int) ((mViewHeight - height * scale) / 2); 805 | } else { 806 | mPhotoLeft = (int) ((mViewWidth - width * scale) / 2); 807 | mPhotoTop = 0; 808 | } 809 | matrix.postScale(scale, scale); 810 | 811 | Bitmap bitmap = mPhotoBitmap; 812 | mPhotoBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true); 813 | mPhotoWidth = mPhotoBitmap.getWidth(); 814 | mPhotoHeight = mPhotoBitmap.getHeight(); 815 | 816 | mCutoutBitmap = Bitmap.createBitmap(mPhotoWidth, mPhotoHeight, Bitmap.Config.ARGB_8888); 817 | } 818 | 819 | public void setMode(@Mode int mode) { 820 | if (mMode == mode) { 821 | return; 822 | } 823 | 824 | mMode = mode; 825 | if (mode == Mode.ERASER && mCurrentRecord != null) { 826 | mCurrentRecord.clearPointList(); 827 | isActionTrackVisible = false; 828 | notifyCanBack(); 829 | postInvalidate(); 830 | } else if (mode == Mode.CUT_OUT) { 831 | mCurrentPath = new Path(); 832 | mPathMeasure.setPath(mCurrentPath, false); 833 | } 834 | } 835 | 836 | 837 | public void setPhoto(String path) { 838 | File file = new File(path); 839 | if (!file.exists() || !file.isFile()) { 840 | Toast.makeText(getContext(), R.string.file_not_exist, Toast.LENGTH_SHORT).show(); 841 | return; 842 | } 843 | CutoutRecord record = new CutoutRecord(); 844 | record.setImagePath(path); 845 | 846 | mMatrix.reset(); 847 | mCutoutRecordList.clear(); 848 | mCutoutRecordList.add(record); 849 | mCurrentRecord = record; 850 | resetPhotoRectF(); 851 | 852 | mPhotoBitmap = BitmapFactory.decodeFile(path); 853 | mPhotoBitmap = ImageUtils.rotateBitmap(ImageUtils.getExifOrientation(path), mPhotoBitmap); 854 | computePhotoInfo(); 855 | postInvalidate(); 856 | } 857 | 858 | 859 | private Runnable mResultCutoutRunnable = new Runnable() { 860 | @Override 861 | public void run() { 862 | notifyShowLoading(); 863 | 864 | Bitmap bitmap = Bitmap.createBitmap(mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), Bitmap.Config.ARGB_8888); 865 | Bitmap pathBitmap = getPathBitmap(); 866 | 867 | Canvas canvas = new Canvas(bitmap); 868 | mPhotoPaint.setXfermode(null); 869 | canvas.drawBitmap(mPhotoBitmap, 0, 0, mPhotoPaint); 870 | mPhotoPaint.setXfermode(mCutoutPorterMode); 871 | canvas.drawBitmap(pathBitmap, 0, 0, mPhotoPaint); 872 | mPhotoPaint.setXfermode(null); 873 | 874 | recycleBitmap(pathBitmap); 875 | recycleBitmap(mPhotoBitmap); 876 | mPhotoBitmap = bitmap; 877 | 878 | isActionTrackVisible = false; 879 | 880 | String path = saveResult(false); 881 | CutoutRecord record = new CutoutRecord(); 882 | record.setImagePath(path); 883 | mCutoutRecordList.add(record); 884 | mCurrentRecord = record; 885 | 886 | notifyCanBack(); 887 | notifyDismissLoading(); 888 | postInvalidate(); 889 | } 890 | }; 891 | 892 | 893 | /** 894 | * 回退 895 | * 备注: 896 | * 回退分3个步骤分别为 897 | * 1. 回退抠图时候的路径 898 | * 2. 回退擦除时候的路径 899 | * 3. 回退抠图 900 | */ 901 | public void back() { 902 | if (mCurrentRecord == null) { 903 | Log.i(TAG, "back: mCurrentRecord is null"); 904 | return; 905 | } 906 | List cutoutTrackList = mCurrentRecord.getCutoutTrackList(); 907 | List eraserPatList = mCurrentRecord.getEraserPathList(); 908 | if (!cutoutTrackList.isEmpty()) { 909 | backCutoutAction(cutoutTrackList); 910 | } else if (!eraserPatList.isEmpty()) { 911 | backEraserPath(eraserPatList, mCurrentRecord.getEraserMatrixList()); 912 | } else if (!mCutoutRecordList.isEmpty()) { 913 | backRecord(); 914 | } 915 | } 916 | 917 | 918 | private void backRecord() { 919 | if (mCutoutRecordList.size() <= 1) { 920 | return; 921 | } 922 | final int size = mCutoutRecordList.size(); 923 | int last = size - 1; 924 | mCutoutRecordList.remove(last); 925 | mCurrentRecord = mCutoutRecordList.get(last - 1); 926 | mPhotoBitmap = BitmapFactory.decodeFile(mCurrentRecord.getImagePath()); 927 | 928 | if (size == 2) { 929 | computePhotoInfo(); 930 | notifyCanBack(); 931 | } 932 | List pathList = mCurrentRecord.getEraserPathList(); 933 | if (!pathList.isEmpty()) { 934 | // 方法默认是回退一个的, 但是现在回退到record 的时候不需要回退这么多 935 | mActionPaint.setAlpha(255); 936 | pathList.add(new Path()); 937 | backEraserPath(pathList, mCurrentRecord.getEraserMatrixList()); 938 | } 939 | 940 | // 擦除的path 给画上去 941 | postInvalidate(); 942 | } 943 | 944 | private void backEraserPath(List pathList, List matrixList) { 945 | final int size = pathList.size(); 946 | recycleBitmap(mPhotoBitmap); 947 | mPhotoBitmap = BitmapFactory.decodeFile(mCurrentRecord.getImagePath()); 948 | computePhotoInfo(); 949 | if (size == 1) { 950 | mCurrentRecord.clearEraserInfo(); 951 | notifyCanBack(); 952 | } else { 953 | pathList.remove(pathList.size() - 1); 954 | matrixList.remove(matrixList.size() - 1); 955 | Bitmap pathBitmap = getBackEraserPathBitmap(pathList, matrixList); 956 | Bitmap bitmap = Bitmap.createBitmap(mPhotoBitmap.getWidth(), mPhotoBitmap.getHeight(), Bitmap.Config.ARGB_8888); 957 | Canvas canvas = new Canvas(bitmap); 958 | mPhotoPaint.setXfermode(null); 959 | canvas.drawBitmap(mPhotoBitmap, 0, 0, mPhotoPaint); 960 | mPhotoPaint.setXfermode(mEraserPorterMode); 961 | canvas.drawBitmap(pathBitmap, 0, 0, mPhotoPaint); 962 | mPhotoPaint.setXfermode(null); 963 | 964 | recycleBitmap(mPhotoBitmap); 965 | recycleBitmap(pathBitmap); 966 | mPhotoBitmap = bitmap; 967 | } 968 | 969 | postInvalidate(); 970 | } 971 | 972 | private void backCutoutAction(List cutoutTrackList) { 973 | final int size = cutoutTrackList.size(); 974 | if (size == 1) { 975 | cutoutTrackList.clear(); 976 | mCurrentPath.reset(); 977 | mPathMeasure.setPath(mCurrentPath, false); 978 | notifyCanBack(); 979 | } else { 980 | Path path = new Path(); 981 | path.moveTo(mDownPoint[0], mDownPoint[1]); 982 | float length = cutoutTrackList.get(size - 2); 983 | mPathMeasure.getSegment(0, length, path, false); 984 | mCurrentPath = path; 985 | mPathMeasure.setPath(mCurrentPath, false); 986 | cutoutTrackList.remove(size - 1); 987 | } 988 | postInvalidate(); 989 | } 990 | 991 | 992 | /** 993 | * 保存图片 994 | */ 995 | @SuppressWarnings("unused") 996 | public String saveResult() { 997 | return saveResult(true); 998 | } 999 | 1000 | 1001 | private String saveResult(boolean recycle) { 1002 | 1003 | File cache = generateRandomPhotoFile(getContext()); 1004 | try { 1005 | FileOutputStream out = new FileOutputStream(cache); 1006 | mPhotoBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 1007 | out.flush(); 1008 | out.close(); 1009 | Log.e(TAG, cache.getAbsolutePath()); 1010 | return cache.getAbsolutePath(); 1011 | } catch (Exception e) { 1012 | e.printStackTrace(); 1013 | return ""; 1014 | } finally { 1015 | if (recycle) { 1016 | 1017 | if (mPhotoBitmap != null) { 1018 | mPhotoBitmap.recycle(); 1019 | mPhotoBitmap = null; 1020 | } 1021 | } 1022 | } 1023 | } 1024 | 1025 | private OnCutoutListener mCutoutListener; 1026 | 1027 | public void setOnCutoutListener(OnCutoutListener listener) { 1028 | mCutoutListener = listener; 1029 | } 1030 | 1031 | private void notifyCanBack() { 1032 | if (mCutoutListener == null) { 1033 | return; 1034 | } 1035 | post(new Runnable() { 1036 | @Override 1037 | public void run() { 1038 | if (mCutoutRecordList.size() > 1 || (mCurrentRecord != null && mCurrentRecord.hasRecord())) { 1039 | mCutoutListener.canBack(1); 1040 | } else { 1041 | mCutoutListener.canBack(0); 1042 | } 1043 | } 1044 | }); 1045 | 1046 | } 1047 | 1048 | private void notifyShowLoading() { 1049 | if (mCutoutListener == null) { 1050 | return; 1051 | } 1052 | post(new Runnable() { 1053 | @Override 1054 | public void run() { 1055 | mCutoutListener.showLoading(); 1056 | 1057 | } 1058 | }); 1059 | } 1060 | 1061 | private void notifyDismissLoading() { 1062 | if (mCutoutListener == null) { 1063 | return; 1064 | } 1065 | post(new Runnable() { 1066 | @Override 1067 | public void run() { 1068 | mCutoutListener.dismissLoading(); 1069 | } 1070 | }); 1071 | } 1072 | 1073 | 1074 | /** 1075 | * 生成图片名称 1076 | */ 1077 | public static File generateRandomPhotoFile(Context context) { 1078 | return new File(generateRandomPhotoName(context)); 1079 | } 1080 | 1081 | /** 1082 | * 生成随机的名字 1083 | */ 1084 | public static String generateRandomPhotoName(Context context) { 1085 | File cacheFolder = context.getExternalCacheDir(); 1086 | if (null == cacheFolder) { 1087 | File target = Environment.getExternalStorageDirectory(); 1088 | cacheFolder = new File(target + File.separator + "Pictures"); 1089 | } 1090 | 1091 | Log.d(TAG, "cacheFolder path = " + cacheFolder.getAbsolutePath()); 1092 | if (!cacheFolder.exists()) { 1093 | try { 1094 | boolean result = cacheFolder.mkdir(); 1095 | Log.d(TAG, " result: " + (result ? "succeeded" : "failed")); 1096 | } catch (Exception e) { 1097 | Log.e(TAG, "generateUri failed: " + e.toString()); 1098 | } 1099 | } 1100 | String name = StringUtils.plusString(UUID.randomUUID().toString().toUpperCase(), ".png"); 1101 | return StringUtils.plusString(cacheFolder.getAbsolutePath(), File.separator, name); 1102 | } 1103 | 1104 | 1105 | private void recycleBitmap(Bitmap bitmap) { 1106 | if (bitmap != null) { 1107 | bitmap.recycle(); 1108 | } 1109 | 1110 | } 1111 | 1112 | private void resetPhotoRectF() { 1113 | mPhotoRectF.left = mPhotoLeft; 1114 | mPhotoRectF.top = mPhotoTop; 1115 | mPhotoRectF.right = mPhotoRectF.left + mPhotoWidth; 1116 | mPhotoRectF.bottom = mPhotoRectF.top + mPhotoHeight; 1117 | } 1118 | 1119 | private float getValues(int index) { 1120 | float[] values = new float[9]; 1121 | mMatrix.getValues(values); 1122 | return values[index]; 1123 | } 1124 | 1125 | private float getValues(Matrix matrix, int index) { 1126 | float[] values = new float[9]; 1127 | matrix.getValues(values); 1128 | return values[index]; 1129 | } 1130 | 1131 | private ValueAnimator.AnimatorUpdateListener mAniListener = new ValueAnimator.AnimatorUpdateListener() { 1132 | @Override 1133 | public void onAnimationUpdate(ValueAnimator animation) { 1134 | Object scaleOb = animation.getAnimatedValue("scale"); 1135 | if (scaleOb != null) { 1136 | float scale = (float) scaleOb; 1137 | float lastScale = getValues(Matrix.MSCALE_X); 1138 | scale = scale / lastScale; 1139 | mMatrix.postScale(scale, scale, mTouchTwoPointCenter[0], mTouchTwoPointCenter[1]); 1140 | } 1141 | 1142 | float lastTranslateX = getValues(Matrix.MTRANS_X); 1143 | float lastTranslateY = getValues(Matrix.MTRANS_Y); 1144 | 1145 | Object translateXOb = animation.getAnimatedValue("translateX"); 1146 | float translateX; 1147 | if (translateXOb != null) { 1148 | translateX = (float) translateXOb - lastTranslateX; 1149 | } else { 1150 | translateX = 0; 1151 | } 1152 | 1153 | Object translateYOb = animation.getAnimatedValue("translateY"); 1154 | float translateY; 1155 | if (translateYOb != null) { 1156 | translateY = (float) translateYOb - lastTranslateY; 1157 | } else { 1158 | translateY = 0; 1159 | } 1160 | 1161 | if (translateX != 0 || translateY != 0) { 1162 | mMatrix.postTranslate(translateX, translateY); 1163 | } 1164 | postInvalidate(); 1165 | } 1166 | }; 1167 | 1168 | private float[] computeRealPoint(float[] point) { 1169 | float[] real = new float[2]; 1170 | mExchangedMatrix.reset(); 1171 | mMatrix.invert(mExchangedMatrix); 1172 | Log.i(TAG, "computeRealPoint: mExchangedMatrix = " + mExchangedMatrix); 1173 | mExchangedMatrix.mapPoints(real, point); 1174 | Log.i(TAG, "computeRealPoint: real x = " + real[0] + " , real y = " + real[1]); 1175 | return real; 1176 | } 1177 | 1178 | private float[] computeNowPoint(float[] point) { 1179 | float[] now = new float[2]; 1180 | mMatrix.mapPoints(now, point); 1181 | return now; 1182 | } 1183 | 1184 | /** 1185 | * 定义图片是单选还是多选 1186 | */ 1187 | @SuppressWarnings("WeakerAccess") 1188 | @IntDef({Mode.CUT_OUT, Mode.ERASER}) 1189 | @Retention(RetentionPolicy.SOURCE) 1190 | public @interface Mode { 1191 | /** 1192 | * 单选 1193 | */ 1194 | int CUT_OUT = 1; 1195 | /** 1196 | * 多选 1197 | */ 1198 | int ERASER = 2; 1199 | } 1200 | 1201 | 1202 | @SuppressWarnings("WeakerAccess") 1203 | public interface OnCutoutListener { 1204 | 1205 | void canBack(int count); 1206 | 1207 | void showLoading(); 1208 | 1209 | void dismissLoading(); 1210 | } 1211 | 1212 | } 1213 | -------------------------------------------------------------------------------- /app/src/main/res/color/normal_grey_selected_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/imitate_transparent_bg_piece.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i-show/android-CutoutView/f7b41e552f9bdb681e62cfa16832d72b63ec582a/app/src/main/res/drawable-xhdpi/imitate_transparent_bg_piece.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i-show/android-CutoutView/f7b41e552f9bdb681e62cfa16832d72b63ec582a/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |