前言
想要好看的龙年头像框网上不是要VIP就是收费,找一圈还找不到自己想要的,索性自己开发一款可以生成任意自己想要的头像框的软件。
效果图
代码实现
布局代码
ini
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/rl"
android:layout_width="300dp"
android:layout_height="300dp">
<ImageView
android:id="@+id/ivHead"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="centerCrop"
android:src="@drawable/a" />
<ImageView
android:id="@+id/ivHeadBorder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/a1" />
</RelativeLayout>
<Button
android:id="@+id/btnHead"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/shape_bg"
android:text="更换头像"
android:textColor="@color/white" />
<Button
android:id="@+id/btnHeadBorder"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/shape_bg"
android:text="更换头像框"
android:textColor="@color/white" />
<Button
android:id="@+id/btnSave"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/shape_bg"
android:text="保存头像"
android:textColor="@color/white" />
</LinearLayout>
页面预览
实现更换头像和头像框
首先更换头像和头像显示用到了Glide和头像选择依赖库pictureselector
build.gradle.kts内添加如下依赖
scss
dependencies {
...
//glide
implementation("com.github.bumptech.glide:glide:4.15.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.15.0")
//图片选择框架
implementation("io.github.lucksiege:pictureselector:v2.7.3-rc08")
}
并且用到了viewBinding,添加viewBinding配置
ini
android{
viewBinding {
enable = true
}
}
更换头像和保存头像用到了拍照和文件存储权限,所以在AndroidManifest.xml中添加拍照权限和文件的写权限
xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!--写权限-->
<uses-permission
android:name="android.permission.CAMERA"
tools:ignore="PermissionImpliesUnsupportedChromeOsHardware" /> <!-- 拍照权限 -->
更换头像和头像框点击事件
less
//更换头像
binding!!.btnHead.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
//没有权限则申请权限
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
202
)
} else {
//有权限直接执行
chooseHead()
}
} else {
//小于6.0,不用申请权限,直接执行
chooseHead()
}
}
//更换头像框
binding!!.btnHeadBorder.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
//没有权限则申请权限
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
203
)
} else {
//有权限直接执行
chooseHeadBorder()
}
} else {
//小于6.0,不用申请权限,直接执行
chooseHeadBorder()
}
}
申请权限回调
kotlin
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
201 -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
draw
} else {
Toast.makeText(
this,
"您拒绝了权限的申请,可能无法进行下面的操作哦~",
Toast.LENGTH_LONG
).show()
}
202 -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
chooseHead()
} else {
Toast.makeText(
this,
"您拒绝了权限的申请,可能无法进行下面的操作哦~",
Toast.LENGTH_LONG
).show()
}
203 -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
chooseHeadBorder()
} else {
Toast.makeText(
this,
"您拒绝了权限的申请,可能无法进行下面的操作哦~",
Toast.LENGTH_LONG
).show()
}
else -> {}
}
}
有权限直接调用选择图片和拍照
kotlin
private fun chooseHead() {
PictureSelectorUtils.ofImage(this@MainActivity, codeHead)
}
private fun chooseHeadBorder() {
PictureSelectorUtils.ofImage(this@MainActivity, codeHeadBorder)
}
PictureSelectorUtils选择图片工具封装
kotlin
package com.cwj.dragon.util
import android.app.Activity
import android.content.Intent
import com.cwj.dragon.R
import com.cwj.dragon.util.GlideEngine.Companion.createGlideEngine
import com.luck.picture.lib.PictureSelector
import com.luck.picture.lib.config.PictureConfig
import com.luck.picture.lib.config.PictureMimeType
/**
* created by cwj on 2024-1-22
* Description:
*/
object PictureSelectorUtils {
fun ofImage(activity: Activity?, requestCode: Int) {
PictureSelector.create(activity)
.openGallery(PictureMimeType.ofImage())
.imageEngine(createGlideEngine())
.theme(R.style.PictureSelectorStyle)
.selectionMode(PictureConfig.SINGLE)
.enableCrop(false) //是否裁剪
.isDragFrame(false) // 是否可拖动裁剪框(固定)
.withAspectRatio(1, 1) // int 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
.isCamera(true) //是否显示拍照按钮 true or false
.isGif(true) //是否显示gif图片 true or false
.previewImage(true) // 是否可预览图片 true or false
.forResult(requestCode)
}
fun forResult(resultCode: Int, data: Intent?): String? {
if (resultCode == Activity.RESULT_OK) {
// 图片、视频、音频选择结果回调
val selectList = PictureSelector.obtainMultipleResult(data)
if (selectList != null && selectList.size > 0) {
return selectList[0].path
}
}
return null
}
}
GlideEngine工具
kotlin
package com.cwj.dragon.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.BitmapImageViewTarget
import com.bumptech.glide.request.target.ImageViewTarget
import com.cwj.dragon.R
import com.luck.picture.lib.engine.ImageEngine
import com.luck.picture.lib.listener.OnImageCompleteCallback
import com.luck.picture.lib.tools.MediaUtils
import com.luck.picture.lib.widget.longimage.ImageSource
import com.luck.picture.lib.widget.longimage.ImageViewState
import com.luck.picture.lib.widget.longimage.SubsamplingScaleImageView
/**
* created by cwj on 2024-1-22
* Description:
*/
class GlideEngine private constructor() : ImageEngine {
/**
* 加载图片
*
* @param context
* @param url
* @param imageView
*/
override fun loadImage(context: Context, url: String, imageView: ImageView) {
if (!ImageLoaderUtils.assertValidRequest(context)) {
return
}
Glide.with(context)
.load(url)
.into(imageView)
}
/**
* 加载网络图片适配长图方案
* # 注意:此方法只有加载网络图片才会回调
*
* @param context
* @param url
* @param imageView
* @param longImageView
* @param callback 网络图片加载回调监听 {link after version 2.5.1 Please use the #OnImageCompleteCallback#}
*/
override fun loadImage(
context: Context, url: String,
imageView: ImageView,
longImageView: SubsamplingScaleImageView, callback: OnImageCompleteCallback
) {
if (!ImageLoaderUtils.assertValidRequest(context)) {
return
}
Glide.with(context)
.asBitmap()
.load(url)
.into(object : ImageViewTarget<Bitmap?>(imageView) {
override fun onLoadStarted(placeholder: Drawable?) {
super.onLoadStarted(placeholder)
callback.onShowLoading()
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
callback.onHideLoading()
}
override fun setResource(resource: Bitmap?) {
callback.onHideLoading()
if (resource != null) {
val eqLongImage = MediaUtils.isLongImg(
resource.width,
resource.height
)
longImageView.visibility = if (eqLongImage) View.VISIBLE else View.GONE
imageView.visibility = if (eqLongImage) View.GONE else View.VISIBLE
if (eqLongImage) {
// 加载长图
longImageView.isQuickScaleEnabled = true
longImageView.isZoomEnabled = true
longImageView.setDoubleTapZoomDuration(100)
longImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)
longImageView.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
longImageView.setImage(
ImageSource.cachedBitmap(resource),
ImageViewState(0f, PointF(0f, 0f), 0)
)
} else {
// 普通图片
imageView.setImageBitmap(resource)
}
}
}
})
}
/**
* 加载网络图片适配长图方案
* # 注意:此方法只有加载网络图片才会回调
*
* @param context
* @param url
* @param imageView
* @param longImageView
* @ 已废弃
*/
@Deprecated("Deprecated in Java")
override fun loadImage(
context: Context, url: String,
imageView: ImageView,
longImageView: SubsamplingScaleImageView
) {
if (!ImageLoaderUtils.assertValidRequest(context)) {
return
}
Glide.with(context)
.asBitmap()
.load(url)
.into(object : ImageViewTarget<Bitmap?>(imageView) {
override fun setResource(resource: Bitmap?) {
if (resource != null) {
val eqLongImage = MediaUtils.isLongImg(
resource.width,
resource.height
)
longImageView.visibility = if (eqLongImage) View.VISIBLE else View.GONE
imageView.visibility = if (eqLongImage) View.GONE else View.VISIBLE
if (eqLongImage) {
// 加载长图
longImageView.isQuickScaleEnabled = true
longImageView.isZoomEnabled = true
longImageView.setDoubleTapZoomDuration(100)
longImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)
longImageView.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
longImageView.setImage(
ImageSource.cachedBitmap(resource),
ImageViewState(0f, PointF(0f, 0f), 0)
)
} else {
// 普通图片
imageView.setImageBitmap(resource)
}
}
}
})
}
/**
* 加载相册目录
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
override fun loadFolderImage(context: Context, url: String, imageView: ImageView) {
if (!ImageLoaderUtils.assertValidRequest(context)) {
return
}
Glide.with(context)
.asBitmap()
.load(url)
.override(180, 180)
.centerCrop()
.sizeMultiplier(0.5f)
.placeholder(R.drawable.ic_launcher_background)
.into(object : BitmapImageViewTarget(imageView) {
override fun setResource(resource: Bitmap?) {
val circularBitmapDrawable =
RoundedBitmapDrawableFactory.create(context.resources, resource)
circularBitmapDrawable.cornerRadius = 8f
imageView.setImageDrawable(circularBitmapDrawable)
}
})
}
/**
* 加载gif
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
override fun loadAsGifImage(
context: Context, url: String,
imageView: ImageView
) {
if (!ImageLoaderUtils.assertValidRequest(context)) {
return
}
Glide.with(context)
.asGif()
.load(url)
.into(imageView)
}
/**
* 加载图片列表图片
*
* @param context 上下文
* @param url 图片路径
* @param imageView 承载图片ImageView
*/
override fun loadGridImage(context: Context, url: String, imageView: ImageView) {
if (!ImageLoaderUtils.assertValidRequest(context)) {
return
}
Glide.with(context)
.load(url)
.override(200, 200)
.centerCrop()
.placeholder(R.drawable.ic_launcher_background)
.into(imageView)
}
companion object {
private var instance: GlideEngine? = null
@JvmStatic
fun createGlideEngine(): GlideEngine? {
if (null == instance) {
synchronized(GlideEngine::class.java) {
if (null == instance) {
instance = GlideEngine()
}
}
}
return instance
}
}
}
ImageLoaderUtils工具
kotlin
package com.cwj.dragon.util
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
/**
* created by cwj on 2024-1-22
* Description:
*/
object ImageLoaderUtils {
fun assertValidRequest(context: Context?): Boolean {
if (context is Activity) {
return !isDestroy(context)
} else if (context is ContextWrapper) {
if (context.baseContext is Activity) {
val activity = context.baseContext as Activity
return !isDestroy(activity)
}
}
return true
}
private fun isDestroy(activity: Activity?): Boolean {
return if (activity == null) {
true
} else activity.isFinishing || activity.isDestroyed
}
}
图片选择成功后页面回显
kotlin
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == codeHead && resultCode == RESULT_OK) {
val head = PictureSelectorUtils.forResult(resultCode, data)
if (head != null) {
Glide.with(this).load(head).into(binding!!.ivHead)
}
} else if (requestCode == codeHeadBorder && resultCode == RESULT_OK) {
val headBorder = PictureSelectorUtils.forResult(resultCode, data)
if (headBorder != null) {
Glide.with(this).load(headBorder).into(binding!!.ivHeadBorder)
}
}
}
保存合成的带头像框的头像
保存图片权限判断和保存图片
less
//保存头像
binding!!.btnSave.setOnClickListener {
if (ContextCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
201
)
} else {
draw
}
}
private val draw: Unit
get() {
// 获取图片某布局
binding!!.rl.isDrawingCacheEnabled = true
binding!!.rl.buildDrawingCache()
mHandler.postDelayed({
// 要在运行在子线程中
val bmp = binding!!.rl.drawingCache // 获取图片
savePicture(bmp) // 保存图片
binding!!.rl.destroyDrawingCache() // 保存过后释放资源
}, 100)
}
保存合成头像图片到本地相册
kotlin
private fun savePicture(bm: Bitmap) {
val file = createImageFile()
//重新写入文件
try {
// 写入文件
val fos = FileOutputStream(file)
//默认jpg
bm.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
fos.close()
bm.recycle()
sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)))
Toast.makeText(this, "保存成功,请到相册中查看!", Toast.LENGTH_LONG).show()
} catch (e: Exception) {
e.printStackTrace()
}
}
companion object {
fun createImageFile(): File {
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"Pic"
)
if (!dir.exists()) {
val isSuccess = dir.mkdirs()
Log.i("Pic", "文件夹创建状态--->$isSuccess")
}
return File(dir.path + File.separator + "img_" + System.currentTimeMillis() + ".png")
}
}
如果不想相册中选择头像框还可以点击头像框随机切换提供的头像框
ini
private val headBorder = intArrayOf(R.drawable.a1, R.drawable.a2)
private var i = 0
...
//点击头像框更改头像框
binding!!.ivHeadBorder.setOnClickListener {
i++
if (headBorder.size == i) {
i = 0
}
binding!!.ivHeadBorder.setImageResource(headBorder[i])
}
目前项目中只提供两张和龙相关的头像框,如有需要请下载头像框。如果想要更多随机切换头像框请运行项目自行网上寻找头像框相关图片并添加多张头像框图片到drawable-xxhdpi文件下并在项目代码中添加引用即可。
项目地址:
软件下载体验地址: