AndroidStudio实现在图片上涂鸦,并保存涂鸦轨迹
开个坑,终于有时间整理一下这个项目里用到的比较重要的技术
虽然最后甲方没有采用(笑)
因为博主学艺不精,有很多小bug
AndroidStudio版本:2020.3.1.25
实现效果:
本文通过重写view类,实现在选择的图片上涂鸦的功能,因为项目需要残留了一些多余代码
项目结构:
MainActivity为主程序类
HandWrite类为手写类,用于处理各种手势
res/layout/activity_main.xml 为主界面
res/xml/file_path.xml 为图片缓存路径
0.Handwrite类
划重点,此类中重写了view类,画出了手指按下的轨迹,并记录在pointlist中文章来源:https://www.toymoban.com/news/detail-485067.html
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方法
@Override
protected 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);
//lasso
if (isMove&&(pen_type==3)){
canvas.drawLine(startX,startY,clickX,clickY,paint); // 在画布上画线条
}
//eraser
if (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;
//pen
if (pen_type==1){
if(isDown){
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(clickX,clickY,20.0f,paint);
}
}
//点击的eraser
if (isDown&&pen_type==2){
paint.setColor(Color.BLUE);
canvas.drawCircle(clickX,clickY,20.0f,paint);
}
if (isClear){
return new_2Bit; // 返回新绘制的图像
}
return origBit; // 若清屏,则返回原图像
}
@Override
public 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal">
</LinearLayout>
<LinearLayout
android:id="@+id/hw"
android:layout_width="300dp"
android:layout_height="500dp"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"></LinearLayout>
<TextView
android: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>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="100dp"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:layout_width="@dimen/icon_loc"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/icon_lasso"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:background="@drawable/ic_lasso"></Button>
<TextView
android: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>
<LinearLayout
android:layout_width="@dimen/icon_loc"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/icon_pen"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:background="@drawable/ic_pen"></Button>
<TextView
android: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>
<LinearLayout
android:layout_width="@dimen/icon_loc"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android: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>
<TextView
android: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" />
<application
android: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">
<activity
android: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>
<provider
android:authorities="com.buildmaterialapplication.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android: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套索
@Override
protected 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();
//aiAlgorithm
buttonEvent();
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() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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)
@Override
public 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的话,尽量不要用这么底层的方法去写文章来源地址https://www.toymoban.com/news/detail-485067.html
到了这里,关于AndroidStudio实现在图片上涂鸦并记录涂鸦轨迹的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!