龙年大吉,是时候换一波龙年头像框了

前言

想要好看的龙年头像框网上不是要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文件下并在项目代码中添加引用即可。

项目地址:

gitee.com/juer2017/dr...

软件下载体验地址:

gitee.com/juer2017/dr...

相关推荐
松仔log3 小时前
Java中级——组合和继承
android·java·开发语言
我命由我123456 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2337 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759627 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马7 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone8 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone8 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子11 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker11 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone12 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常