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

前言

想要好看的龙年头像框网上不是要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...

相关推荐
恋猫de小郭2 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker7 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴7 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭17 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab19 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋1 天前
Android 协程时代,Handler 应该退休了吗?
android