需要图片集和源码请点赞关注收藏后评论区留言~~~
一、需求描述
大头贴App有两个特征,第一个是头要大,拿来一张照片后把人像区域裁剪出来,这样新图片里的人头才会比较大,第二个是在周围贴上装饰物,而且装饰物还能随时更换,这样一副人像可以变换出许多张大头贴(类似于现在拍照APP的环境变换功能)
二、功能分析
就人像剪裁功能而言,首先适当变换图片,把人像区域调整到屏幕中央,以便后续的裁剪操作,这些图片变换操作包括平移图片,旋转图片,水平反转图片等等。调整好人像位置以后,再来裁剪指定的人像区域,为了方便观察待裁剪区域和其余的图片区域,可将待裁剪区域高亮显示,同时暗色显示其余的图片区域。
就人像装饰功能而言,可用于装饰的物品包括一段文字,图片标志,相框背景等等。待装饰的人像图片由前一步的人像裁剪而来,装饰完成的人像图片需要支持保存到存储卡,从而实现大头贴的多次加工操作
下面简单介绍一下源码之间的关系 便于理解
1:PortraitActivity 采集头像的活动页面 从原始图片裁剪指定的人像区域
2:PurikuraActivity 制作大头贴的活动页面,给采集来的头像添加各种装饰
采集头像的时候,先把裁剪后的头像保存为图片文件,再将图片文件路径返回给制作页面
三、效果展示
采集头像页面如下 可以选择左右反转上下移动等操作选择图片的不同位置
装饰图像效果如下
可以加上不同的背景和装饰文字
四、代码
采集图像类代码如下
package com.example.picture; import androidx.appcompat.app.AppCompatActivity; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import com.example.picture.util.BitmapUtil; import com.example.picture.util.DateUtil; import com.example.picture.widget.BitmapView; import com.example.picture.widget.CropImageView; public class PortraitActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener { private final static String TAG = "PortraitActivity"; private int COMBINE_CODE = 4; // 既可拍照获得现场图片、也可在相册挑选已有图片的请求码 private BitmapView bv_photo; // 声明一个位图视图对象 private CropImageView civ_photo; // 声明一个裁剪视图对象 private LinearLayout ll_adjust; // 声明一个线性布局对象 private SeekBar sb_scale; // 声明一个拖动条对象 private SeekBar sb_horizontal; // 声明一个拖动条对象 private SeekBar sb_vertical; // 声明一个拖动条对象 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_portrait); TextView tv_title = findViewById(R.id.tv_title); tv_title.setText("采集头像"); TextView tv_option = findViewById(R.id.tv_option); tv_option.setText("确定"); tv_option.setOnClickListener(v -> finishCollect()); bv_photo = findViewById(R.id.bv_photo); civ_photo = findViewById(R.id.civ_photo); ll_adjust = findViewById(R.id.ll_adjust); findViewById(R.id.btn_combine).setOnClickListener(v -> openSelectDialog()); CheckBox ck_flip = findViewById(R.id.ck_flip); ck_flip.setOnCheckedChangeListener((buttonView, isChecked) -> { bv_photo.flip(); // 左右翻转图像 civ_photo.flip(); // 左右翻转图像 }); sb_scale = findViewById(R.id.sb_scale); sb_horizontal = findViewById(R.id.sb_horizontal); sb_vertical = findViewById(R.id.sb_vertical); sb_scale.setOnSeekBarChangeListener(this); sb_horizontal.setOnSeekBarChangeListener(this); sb_vertical.setOnSeekBarChangeListener(this); } // 结束头像采集 private void finishCollect() { if (civ_photo.getCropBitmap() == null) { Toast.makeText(this, "请先选好头像图片", Toast.LENGTH_SHORT).show(); return; } // 生成图片文件的保存路径 String path = String.format("%s/%s.jpg", getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(), DateUtil.getNowDateTime()); BitmapUtil.saveImage(path, civ_photo.getCropBitmap()); // 把位图保存为图片文件 BitmapUtil.notifyPhotoAlbum(this, path); // 通知相册来了张新图片 Intent intent = new Intent(); // 创建一个新意图 intent.putExtra("pic_path", path); setResult(Activity.RESULT_OK, intent); // 携带意图返回前一个页面 finish(); // 关闭当前页面 } // 打开选择对话框(要拍照还是去相册) private void openSelectDialog() { // 声明相机的拍照行为 Intent photoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Intent[] intentArray = new Intent[] { photoIntent }; // 声明相册的打开行为 Intent albumIntent = new Intent(Intent.ACTION_GET_CONTENT); albumIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否允许多选 albumIntent.setType("image/*"); // 类型为图像 // 容纳相机和相册在内的选择意图 Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); chooserIntent.putExtra(Intent.EXTRA_TITLE, "请拍照或选择图片"); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); chooserIntent.putExtra(Intent.EXTRA_INTENT, albumIntent); // 创建封装好标题的选择器意图 Intent chooser = Intent.createChooser(chooserIntent, "选择图片"); // 在页面底部弹出多种选择方式的列表对话框 startActivityForResult(chooser, COMBINE_CODE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK && requestCode == COMBINE_CODE) { // 从组合选择返回 if (intent.getData() != null) { // 从相册选择一张照片 Uri uri = intent.getData(); // 获得已选择照片的路径对象 // 根据指定图片的uri,获得自动缩小后的位图对象 Bitmap bitmap = BitmapUtil.getAutoZoomImage(this, uri); showPicture(bitmap); // 显示已选择的图片 } else if (intent.getExtras() != null) { // 拍照的缩略图 Object obj = intent.getExtras().get("data"); if (obj instanceof Bitmap) { // 属于位图类型 Bitmap bitmap = (Bitmap) obj; // 强制转成位图对象 showPicture(bitmap); // 显示已选择的图片 } } } } // 显示已选择的图片 private void showPicture(Bitmap origin) { bv_photo.setDrawingCacheEnabled(true); // 开启位图视图的绘图缓存 bv_photo.setImageBitmap(origin); // 设置图像视图的位图对象 ll_adjust.setVisibility(View.VISIBLE); sb_scale.setProgress(30); // 设置拖动条的当前进度 sb_horizontal.setProgress(50); // 设置拖动条的当前进度 sb_vertical.setProgress(50); // 设置拖动条的当前进度 Bitmap bitmap = bv_photo.getDrawingCache(); // 从绘图缓存获取位图对象 if (bitmap == null) { return; } int width = bitmap.getWidth(), height = bitmap.getHeight(); civ_photo.setOrigBitmap(bitmap); // 设置裁剪视图的原始位图 // 设置位图的矩形边界 civ_photo.setBitmapRect(new Rect(width/8, height/8, width/4*3, height/4*3)); bv_photo.setDrawingCacheEnabled(false); // 关闭位图视图的绘图缓存 } // 刷新图像展示 private void refreshImage() { Bitmap bitmap = bv_photo.getDrawingCache(); // 从绘图缓存获取位图对象 civ_photo.setOrigBitmap(bitmap); // 设置裁剪视图的原始位图 civ_photo.setBitmapRect(civ_photo.getBitmapRect()); // 设置裁剪视图的位图边界 } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { bv_photo.setDrawingCacheEnabled(true); // 开启位图视图的绘图缓存 if (seekBar.getId() == R.id.sb_scale) { bv_photo.setScaleRatio(progress/33f, true); // 设置位图视图的缩放比率 } else if (seekBar.getId()==R.id.sb_horizontal || seekBar.getId()==R.id.sb_vertical) { int viewWidth = bv_photo.getMeasuredWidth(); // 获取视图的实际宽度 int viewHeight = bv_photo.getMeasuredHeight(); // 获取视图的实际高度 int offsetX = (int) ((sb_horizontal.getProgress()-50)/50f*viewWidth); int offsetY = (int) ((sb_vertical.getProgress()-50)/50f*viewHeight); Log.d(TAG, "viewWidth="+viewWidth+", offsetX="+offsetX+", viewHeight="+viewHeight+", offsetY="+offsetY); bv_photo.setOffset(offsetX, offsetY, true); // 设置位图视图的偏移距离 } refreshImage(); // 刷新图像展示 bv_photo.setDrawingCacheEnabled(false); // 关闭位图视图的绘图缓存 } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) {} }
装饰图像类代码如下
package com.example.picture; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.example.picture.util.BitmapUtil; import com.example.picture.util.DateUtil; import com.example.picture.widget.DecorateImageView; public class PurikuraActivity extends AppCompatActivity { private final static String TAG = "PurikuraActivity"; private int COLLECT_CODE = 14; // 采集头像的请求码 private DecorateImageView div_photo; // 声明一个装饰视图对象 private EditText et_text; // 声明一个编辑框对象 private boolean haveCollected = false; // 是否已采集头像 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_purikura); findViewById(R.id.iv_back).setOnClickListener(v -> finish()); TextView tv_title = findViewById(R.id.tv_title); tv_title.setText("大头贴"); TextView tv_option = findViewById(R.id.tv_option); tv_option.setText("保存"); tv_option.setOnClickListener(v -> savePurikura()); div_photo = findViewById(R.id.div_photo); et_text = findViewById(R.id.et_text); findViewById(R.id.btn_text).setOnClickListener(v -> { String text = et_text.getText().toString(); div_photo.showText(text, false); // 显示装饰文本 }); findViewById(R.id.btn_collect).setOnClickListener(v -> { // 下面跳到头像采集页面 Intent intent = new Intent(this, PortraitActivity.class); startActivityForResult(intent, COLLECT_CODE); }); initLogoSpinner(); // 初始化标志图片下拉框 initFrameSpinner(); // 初始化相框种类下拉框 } // 保存加工好的大头贴图片 private void savePurikura() { if (!haveCollected) { Toast.makeText(this, "请先采集头像图片", Toast.LENGTH_SHORT).show(); return; } div_photo.setDrawingCacheEnabled(true); // 开启装饰视图的绘图缓存 Bitmap bitmap = div_photo.getDrawingCache(); // 从绘图缓存获取位图对象 // 生成图片文件的保存路径 String path = String.format("%s/%s.jpg", getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(), DateUtil.getNowDateTime()); BitmapUtil.saveImage(path, bitmap); // 把位图保存为图片文件 BitmapUtil.notifyPhotoAlbum(this, path); // 通知相册来了张新图片 Toast.makeText(this, "已保存大头贴图片 "+path, Toast.LENGTH_SHORT).show(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (resultCode == RESULT_OK && requestCode == COLLECT_CODE) { // 从采集头像返回 String pic_path = intent.getStringExtra("pic_path"); div_photo.setImageURI(Uri.parse(pic_path)); // 设置图像视图的路径对象 haveCollected = true; } } // 初始化标志图片下拉框 private void initLogoSpinner() { ArrayAdapter<String> logoAdapter = new ArrayAdapter<>(this, R.layout.item_select, logoNameArray); Spinner sp_logo = findViewById(R.id.sp_logo); sp_logo.setPrompt("请选择标志图片"); sp_logo.setAdapter(logoAdapter); sp_logo.setOnItemSelectedListener(new LogoSelectedListener()); sp_logo.setSelection(0); } private String[] logoNameArray = {"无标志", "春之兰花", "夏之荷花", "秋之菊花", "冬之梅花"}; class LogoSelectedListener implements AdapterView.OnItemSelectedListener { public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { Bitmap bitmap = null; if (arg2 == 1) { // 春之兰花 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower_orchid); } else if (arg2 == 2) { // 夏之荷花 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower_lotus); } else if (arg2 == 3) { // 秋之菊花 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower_chrysanthemum); } else if (arg2 == 4) { // 冬之梅花 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower_plum); } div_photo.showLogo(bitmap, false); // 显示装饰标志 } public void onNothingSelected(AdapterView<?> arg0) {} } // 初始化相框种类下拉框 private void initFrameSpinner() { ArrayAdapter<String> frameAdapter = new ArrayAdapter<>(this, R.layout.item_select, frameNameArray); Spinner sp_frame = findViewById(R.id.sp_frame); sp_frame.setPrompt("请选择相框种类"); sp_frame.setAdapter(frameAdapter); sp_frame.setOnItemSelectedListener(new FrameSelectedListener()); sp_frame.setSelection(0); } private String[] frameNameArray = {"无相框", "长方相框", "椭圆相框", "稻草相框", "爱心相框"}; class FrameSelectedListener implements AdapterView.OnItemSelectedListener { public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { Bitmap bitmap = null; if (arg2 == 1) { // 长方相框 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo_frame1); } else if (arg2 == 2) { // 椭圆相框 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo_frame2); } else if (arg2 == 3) { // 稻草相框 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo_frame3); } else if (arg2 == 4) { // 爱心相框 bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.photo_frame4); } div_photo.showFrame(bitmap, false); // 显示装饰相框 } public void onNothingSelected(AdapterView<?> arg0) {} } }
XML文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include layout="@layout/title_purikura" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="270dp" > <com.example.picture.widget.BitmapView android:id="@+id/bv_photo" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:scaleType="centerCrop" /> <com.example.picture.widget.CropImageView android:id="@+id/civ_photo" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout> <Button android:id="@+id/btn_combine" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="拍照或从相册选取" android:textColor="@color/black" android:textSize="17sp" /> <LinearLayout android:id="@+id/ll_adjust" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical" android:visibility="invisible"> <CheckBox android:id="@+id/ck_flip" android:layout_width="match_parent" android:layout_height="40dp" android:text="是否左右翻转图像" android:textColor="@color/black" android:textSize="17sp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="30dp" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="缩小" android:textColor="@color/black" android:textSize="17sp" /> <SeekBar android:id="@+id/sb_scale" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:max="100" android:progress="30" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="放大" android:textColor="@color/black" android:textSize="17sp" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="30dp" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="左移" android:textColor="@color/black" android:textSize="17sp" /> <SeekBar android:id="@+id/sb_horizontal" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:max="100" android:progress="50" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="右移" android:textColor="@color/black" android:textSize="17sp" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="30dp" android:layout_margin="10dp" android:gravity="center" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="上移" android:textColor="@color/black" android:textSize="17sp" /> <SeekBar android:id="@+id/sb_vertical" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:max="100" android:progress="50" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下移" android:textColor="@color/black" android:textSize="17sp" /> </LinearLayout> </LinearLayout> </LinearLayout>
创作不易 觉得有帮助请点赞关注收藏~~~