AndroidStudio实现在图片上涂鸦,并保存涂鸦轨迹
开个坑,终于有时间整理一下这个项目里用到的比较重要的技术
虽然最后甲方没有采用(笑)
因为博主学艺不精,有很多小bug
AndroidStudio版本:2020.3.1.25
实现效果:
本文通过重写view类,实现在选择的图片上涂鸦的功能,因为项目需要残留了一些多余代码
项目结构:
MainActivity为主程序类
HandWrite类为手写类,用于处理各种手势
res/layout/activity_main.xml 为主界面
res/xml/file_path.xml 为图片缓存路径
0.Handwrite类
划重点,此类中重写了view类,画出了手指按下的轨迹,并记录在pointlist中
package com.buildmaterialapplication;import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;import java.util.ArrayList;public class HandWrite extends View {Paint paint = null; //定义画笔Bitmap origBit = null; //存放原始图像Bitmap new_1Bit = null; //存放从原始图像复制的位图图像Bitmap new_2Bit = null; //存放处理后的图像float startX = 0,startY = 0; //画线的起点坐标float clickX = 0, clickY = 0; //画线的终点坐标boolean isMove = false; //设置是否画线的标记boolean isDown =false;boolean isClear = false; //设置是否清除涂鸦的标记int color = Color.BLUE; //设置画笔的颜色float strokeWidth = 4.0f; //设置画笔的宽度private int pen_type=0;ArrayList<Point> pointList =new ArrayList();Point point=new Point();public HandWrite(Context context, AttributeSet arrs){super(context,arrs);}//设置画笔类型public void setPen_type(int x) {pen_type = x;Log.e("penType",Integer.toString(pen_type));}//构造public HandWrite(Context context, Bitmap bm, int type) {super(context);// 从资源中获取原始图像//origBit = BitmapFactory.decodeFile("/storage/emulated/0/Android/data/com.example.test/cache/19771639479845774.jpg").copy(Bitmap.Config.ARGB_8888,true);origBit=Bitmap.createBitmap(bm).copy(Bitmap.Config.ARGB_8888,true);//origBit = BitmapFactory.decodeResource(getResources(), R.drawable.p1).copy(Bitmap.Config.ARGB_8888,true);// 建立原始图像的位图new_1Bit = Bitmap.createBitmap(origBit);pen_type=type;}public Point getPoint() {return point;}public ArrayList<Point> getPointList() {return pointList;}// 清除涂鸦public void clear() {isClear = true;new_2Bit = Bitmap.createBitmap(origBit);invalidate();}//设置画笔样式public void setSytle(float strokeWidth) {this.strokeWidth = strokeWidth;this.color=Color.BLUE;}//重写ondraw方法@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawBitmap(HandWriting(new_1Bit),0,0,null);}private Bitmap HandWriting(Bitmap origBit) { //记录绘制图形Canvas canvas = null; // 定义画布if (isClear) { // 创建绘制新图形的画布canvas = new Canvas(new_2Bit);}else {canvas = new Canvas(origBit); //创建绘制原图形的画布}paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setAntiAlias(true);paint.setColor(color);paint.setStrokeWidth(strokeWidth);//lassoif (isMove&&(pen_type==3)){canvas.drawLine(startX,startY,clickX,clickY,paint); // 在画布上画线条}//eraserif (isMove&&pen_type==2){paint.setAlpha(70);paint.setStrokeWidth(40.0f);paint.setStyle(Paint.Style.FILL);canvas.drawLine(startX,startY,clickX,clickY,paint); // 在画布上画线条}startX = clickX;startY = clickY;//penif (pen_type==1){if(isDown){paint.setColor(Color.GREEN);paint.setStyle(Paint.Style.FILL);canvas.drawCircle(clickX,clickY,20.0f,paint);}}//点击的eraserif (isDown&&pen_type==2){paint.setColor(Color.BLUE);canvas.drawCircle(clickX,clickY,20.0f,paint);}if (isClear){return new_2Bit; // 返回新绘制的图像}return origBit; // 若清屏,则返回原图像}@Overridepublic boolean onTouchEvent(MotionEvent event) {final int historySize = event.getHistorySize();final int pointerCount = event.getPointerCount();for (int h = 0; h < historySize; h++) {
// Log.e("At time %d:", (event.getHistoricalEventTime(h)));for (int p = 0; p < pointerCount; p++) {/*System.out.printf(" pointer %d: (%f,%f)",event.getPointerId(p), event.getHistoricalX(p, h), event.getHistoricalY(p, h));*/int po=(event.getPointerId(p));Float ghx=event.getHistoricalX(p, h);Float ghy=event.getHistoricalY(p, h);String pointer =Integer.toString(po);String x=Float.toString(ghx);String y=Float.toString(ghy);String tmp1=pointer+x+y;
// Log.e("pointer1",tmp1);}}int time= (int) event.getEventTime();
// String tmp2=Integer.toString(time);
// Log.e("time",tmp2);//System.out.printf("At time %d:", event.getEventTime());//获取点集for (int p = 0; p < pointerCount; p++) {int po=(event.getPointerId(p));int ghx=(int) event.getX(p);int ghy=(int) event.getY(p);Point point=new Point(ghx,ghy);if(!pointList.contains(point)){if(pen_type!=2){pointList.add(p,point);int x=pointList.get(p).x;int y=pointList.get(p).y;Log.e("po",Integer.toString(x)+" "+Integer.toString(y));}}
// String pointer =Integer.toString(po);/*System.out.printf(" pointer %d: (%f,%f)",event.getPointerId(p), event.getX(p), event.getY(p));*/}clickX = event.getX(); // 获取触摸坐标位置clickY = event.getY();if(event.getAction() == MotionEvent.ACTION_DOWN){isMove=false;isDown=true;invalidate();return true;}else if (event.getAction() == MotionEvent.ACTION_MOVE) { // 记录在屏幕上划动的轨迹isMove = true;isDown=false;invalidate();return true;}if(event.getAction()==MotionEvent.ACTION_UP){performClick();invalidate();return true;}return super.onTouchEvent(event);}
}
1.activity_main.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:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".MainActivity"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="50dp"android:gravity="center"android:orientation="horizontal"></LinearLayout><LinearLayoutandroid:id="@+id/hw"android:layout_width="300dp"android:layout_height="500dp"android:layout_gravity="center"android:gravity="center"android:orientation="horizontal"></LinearLayout><TextViewandroid:id="@+id/txt_result"android:layout_gravity="center"android:layout_marginTop="@dimen/space"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="@dimen/txt_choose"android:textColor="@color/black"></TextView><LinearLayoutandroid:layout_width="match_parent"android:layout_height="100dp"android:layout_marginBottom="100dp"android:gravity="center"android:orientation="horizontal"><LinearLayoutandroid:layout_width="@dimen/icon_loc"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"><Buttonandroid:id="@+id/icon_lasso"android:layout_width="@dimen/icon_size"android:layout_height="@dimen/icon_size"android:background="@drawable/ic_lasso"></Button><TextViewandroid:id="@+id/txt_lasso"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="@dimen/space"android:text="@string/txt_lasso"android:textColor="@color/black"android:textSize="@dimen/btn_txt_size"></TextView></LinearLayout><LinearLayoutandroid:layout_width="@dimen/icon_loc"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"><Buttonandroid:id="@+id/icon_pen"android:layout_width="@dimen/icon_size"android:layout_height="@dimen/icon_size"android:background="@drawable/ic_pen"></Button><TextViewandroid:id="@+id/txt_pen"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="@dimen/space"android:text="@string/txt_pen"android:textColor="@color/black"android:textSize="@dimen/btn_txt_size"></TextView></LinearLayout><LinearLayoutandroid:layout_width="@dimen/icon_loc"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"><Buttonandroid:id="@+id/icon_next"android:layout_width="@dimen/icon_size"android:layout_height="@dimen/icon_size"android:gravity="center"android:background="@drawable/ic_next"></Button><TextViewandroid:id="@+id/txt_next"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="@dimen/space"android:text="@string/txt_next"android:textColor="@color/black"android:textSize="@dimen/btn_txt_size"></TextView></LinearLayout></LinearLayout>
</LinearLayout>
2.file_paths.xml图片存储路径
<?xml version="1.0" encoding="utf-8"?>
<resources><external-cache-path path="." name="take_photo"/>
</resources>
3.mainfest文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.buildmaterialapplication"><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"tools:ignore="ProtectedPermissions"/><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.CAMERA"/><uses-permission android:name="android.permission.INTERNET" /><applicationandroid:allowBackup="true"android:icon="@drawable/ic_car"android:label="@string/app_name"android:roundIcon="@drawable/ic_car"android:supportsRtl="true"android:theme="@style/Theme.MyApplication"android:requestLegacyExternalStorage="true"android:usesCleartextTraffic="true"android:hardwareAccelerated="false"android:largeHeap="true"><activityandroid:name=".MainActivity"android:exported="true"tools:ignore="DuplicateActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><providerandroid:authorities="com.buildmaterialapplication.fileprovider"android:name="androidx.core.content.FileProvider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths"/></provider></application><supports-screens android:resizeable="true" />
</manifest>
4.MainActivity
package com.buildmaterialapplication;import android.Manifest;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;import org.jetbrains.annotations.Nullable;
import org.opencv.core.Mat;import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;//阅读前请查看README文件
public class MainActivity extends AppCompatActivity {//spinner用的列表private final static String[] items = new String[]{"拍照","从相册中选择",};public static final int TAKE_PHOTO=1;//声明一个请求码,用于识别返回的结果private static final int SCAN_OPEN_PHONE = 2;// 相册private Uri imageUri;public String path=null;Bitmap bitmap=null;public int count=0;public String picpath=null;private LinearLayout handWrite=null;public HandWrite hd;public HandWrite ori_hd; //当前显示的handwrite控件public boolean isLasso=false; //是否选择范围int requestW=0; //初始化页面显示的图片的宽高int requestH=0;int drawType=0; //画笔类型 1画笔 2橡皮 3套索@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);askPermission();//用于设置图片显示的大小DisplayMetrics dm=getResources().getDisplayMetrics();requestW=(int) (dm.widthPixels*0.8);requestH=(int) (dm.heightPixels*0.6);//加载手写类控件handWrite=findViewById(R.id.hw);choosePic();//aiAlgorithmbuttonEvent();if(drawType==0){TextView txt_pen=findViewById(R.id.txt_pen);txt_pen.setTextColor(Color.BLACK);}}/*** @name: askPermission* @param :null* @return :null* @describe: 请求相机等权限*/private void askPermission(){ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.CAMERA},0);}/*** @name: buttonEvent* @param :null* @return null* @describe: 按钮点击事件*/public void buttonEvent() {Button btn_pen;Button btn_lasso;Button btn_eraser;TextView txt_pen = findViewById(R.id.txt_pen);TextView txt_lasso = findViewById(R.id.txt_lasso);btn_pen = (Button) findViewById(R.id.icon_pen);//画笔事件btn_pen.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {txt_pen.setTextColor(Color.GRAY);txt_lasso.setTextColor(Color.BLACK);drawType = 1;ori_hd.setPen_type(drawType);}});//套索事件btn_lasso = (Button) findViewById(R.id.icon_lasso);btn_lasso.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {txt_lasso.setTextColor(Color.GRAY);txt_pen.setTextColor(Color.BLACK);
// draw_type=0;drawType = 3;ori_hd.setPen_type(drawType);isLasso = true;}});//下一张Button ic_next = (Button) findViewById(R.id.icon_next);ic_next.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {startActivity(new Intent(com.buildmaterialapplication.MainActivity.this, com.buildmaterialapplication.MainActivity.class));}});}/*** @name: choosePic* @params:* @return* @describe: 选择照片或拍照*/private void choosePic(){count=0;AlertDialog.Builder builder = new AlertDialog.Builder(com.buildmaterialapplication.MainActivity.this).setTitle("请选择图片")//设置对话框 标题.setItems(items, new DialogInterface.OnClickListener() {@RequiresApi(api = Build.VERSION_CODES.N)@Overridepublic void onClick(DialogInterface dialog, int which) {if(which==0){openCamera();}else{openGallery();}return;}});builder.create().show();}/*** @name: openGallery* @params:* @return* @describe: 拍照*/private void openGallery() {Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);//intent.setType("image/*");startActivityForResult(intent, SCAN_OPEN_PHONE);}/*** @name: openCamera* @params:* @return* @describe: 拍照*/@RequiresApi(api = Build.VERSION_CODES.N)private void openCamera(){String imageName = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(new Date());
// File outputImage=new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/data/com.example.woundapplication/"+imageName+".jpg");File outputImage = new File(getExternalCacheDir(), imageName+".jpg");Objects.requireNonNull(outputImage.getParentFile()).mkdirs();
// Log.e("", outputImage.getAbsolutePath());/*创建一个File文件对象,用于存放摄像头拍下的图片,我们把这个图片命名为output_image.jpg并把它存放在应用关联缓存目录下,调用getExternalCacheDir()可以得到这个目录,为什么要用关联缓存目录呢?由于android6.0开始,读写sd卡列为了危险权限,使用的时候必须要有权限,应用关联目录则可以跳过这一步*/try//判断图片是否存在,存在则删除在创建,不存在则直接创建{if(outputImage.exists()){outputImage.delete();}boolean a = outputImage.createNewFile();
// Log.e("createNewFile", String.valueOf(a));}catch (IOException e){e.printStackTrace();}if(Build.VERSION.SDK_INT>=24)//判断安卓的版本是否高于7.0,高于则调用高于的方法,低于则调用低于的方法//把文件转换成Uri对象/*之所以这样,是因为android7.0以后直接使用本地真实路径是不安全的,会抛出异常。FileProvider是一种特殊的内容提供器,可以对数据进行保护*/{imageUri= FileProvider.getUriForFile(com.buildmaterialapplication.MainActivity.this,"com.buildmaterialapplication.fileprovider",outputImage);
// imageUri=Uri.fromFile(outputImage);path=imageUri.getPath();Log.e(">7:",path);/*第一个参数:context对象第二个参数:任意唯一的字符串第三个参数:文件对象*/}else {imageUri= Uri.fromFile(outputImage);path=imageUri.getPath();Log.e("<7:",imageUri.getPath());}//使用隐示的Intent,系统会找到与它对应的活动,即调用摄像头,并把它存储Intent intent0=new Intent("android.media.action.IMAGE_CAPTURE");
// Intent intent0=new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);intent0.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);startActivityForResult(intent0,TAKE_PHOTO);//调用会返回结果的开启方式,返回成功的话,则把它显示出来Log.e("pic",path);}//拍照或相册的响应事件@SuppressLint("SetTextI18n")protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {super.onActivityResult(requestCode, resultCode, data);Bitmap bitmaptmp;switch (requestCode) {case TAKE_PHOTO:if (resultCode == RESULT_OK) {//将图片解析成Bitmap对象,并把它显现出来try {bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));} catch (FileNotFoundException e) {e.printStackTrace();}picpath= com.buildmaterialapplication.MainActivity.this.getExternalCacheDir().getPath()+imageUri.getPath();
// bitmap =BitmapFactory.decodeStream(filePath);Log.e("filename",picpath);@SuppressLint("SdCardPath") String fileName = picpath;//缩放bitmap=getScaleBitmap(bitmap,requestW,requestH);Mat m=new Mat();hd=new HandWrite(com.buildmaterialapplication.MainActivity.this,bitmap,0);handWrite.addView(hd);ori_hd=hd;}break;//相册的响应case SCAN_OPEN_PHONE:if (resultCode == RESULT_OK){Uri selectImage=data.getData();String[] FilePathColumn={MediaStore.Images.Media.DATA};Cursor cursor = getContentResolver().query(selectImage,FilePathColumn, null, null, null);cursor.moveToFirst();//从数据视图中获取已选择图片的路径int columnIndex = cursor.getColumnIndex(FilePathColumn[0]);picpath = cursor.getString(columnIndex);Log.e("picpath",picpath);cursor.close();bitmaptmp=BitmapFactory.decodeFile(picpath);bitmap=getScaleBitmap(bitmaptmp,requestW,requestH);//预处理hd=new HandWrite(com.buildmaterialapplication.MainActivity.this,bitmap,0);handWrite.addView(hd);ori_hd=hd;}break;default:break;}}/*** @name: getScaleBitmap* @params:* sourceBitmap:原图* width: 需要的宽* height: 需要的高* @return* @describe: bitmap缩放函数*/private Bitmap getScaleBitmap(Bitmap sourceBitmap,float width,float height){Bitmap scaleBitmap;Matrix matrix = new Matrix();float scale= 0;if(sourceBitmap.getWidth()>sourceBitmap.getHeight()){scale=width/sourceBitmap.getWidth();}else{scale=height/ sourceBitmap.getHeight();}
// float scale_x = width/sourceBitmap.getWidth();
// float scale_y = height/sourceBitmap.getHeight();matrix.postScale(scale,scale);try {scaleBitmap = Bitmap.createBitmap(sourceBitmap,0,0,sourceBitmap.getWidth(),sourceBitmap.getHeight(),matrix,true);}catch (OutOfMemoryError e){scaleBitmap = null;System.gc();}return scaleBitmap;}
}
结语
最后只保留了涂鸦的部分,擦除有点bug
不想读取手机中的照片,想直接用电脑上的照片的话,改掉handwrite类里的构造方法就好了,注释中有写使用本地资源的方法,
代码还是有很多问题的,handwrite类的刷新写的也很麻烦,如果不是学习Android的话,尽量不要用这么底层的方法去写