二维码

This commit is contained in:
2026-05-13 17:46:21 +08:00
parent 80837e39bf
commit 14d1969903
6 changed files with 220 additions and 12 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-05-07T09:25:57.105734500Z"> <DropdownSelection timestamp="2026-05-13T03:52:47.453596600Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Administrator\.android\avd\Pixel_10_Pro.avd" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=AMRF025926116461" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@@ -130,6 +130,16 @@
<activity <activity
android:name=".login.LoginActivity" android:name=".login.LoginActivity"
android:exported="false" /> android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -1,11 +1,15 @@
package com.example.defenseapplication.personal package com.example.defenseapplication.personal
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.os.Bundle
import android.os.Bundle
import android.os.Environment
import android.text.TextPaint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.RadioButton import android.widget.RadioButton
@@ -21,6 +25,8 @@ import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import java.io.File
import java.io.FileOutputStream
import java.util.Hashtable import java.util.Hashtable
//邀请码页面 //邀请码页面
@@ -29,6 +35,11 @@ class InvitationCodeActivity : AppCompatActivity() {
private lateinit var binding: InvitationCodeActivityBinding private lateinit var binding: InvitationCodeActivityBinding
private var expireTimeInMillis: Long = 24 * 60 * 60 * 1000 // 默认24小时 private var expireTimeInMillis: Long = 24 * 60 * 60 * 1000 // 默认24小时
private var selectedExpireOption: Int = 0 // 0: 24h, 1: 7d默认选中24h private var selectedExpireOption: Int = 0 // 0: 24h, 1: 7d默认选中24h
private var generatedInviteCode: String = "" // 生成的邀请码
private var expireTimestamp: Long = 0 // 过期时间戳
private var qrCodeContent: String = "" // 生成的二维码内容
private var lastClickTime: Long = 0 // 上次点击时间,用于防止多次点击
private var expireTimeDialog: BottomSheetDialog? = null // 时效选择弹窗
companion object { companion object {
const val EXPIRE_24H = 24 * 60 * 60 * 1000L const val EXPIRE_24H = 24 * 60 * 60 * 1000L
@@ -62,6 +73,12 @@ class InvitationCodeActivity : AppCompatActivity() {
} }
binding.llWechatShare.setOnClickListener { binding.llWechatShare.setOnClickListener {
// 防止多次点击间隔1秒
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < 3000) {
return@setOnClickListener
}
lastClickTime = currentTime
// 微信分享功能 // 微信分享功能
shareToWeChat() shareToWeChat()
} }
@@ -70,8 +87,8 @@ class InvitationCodeActivity : AppCompatActivity() {
private fun showExpireTimeDialog() { private fun showExpireTimeDialog() {
val dialogBinding = DialogExpireTimeBinding.inflate(LayoutInflater.from(this)) val dialogBinding = DialogExpireTimeBinding.inflate(LayoutInflater.from(this))
val dialog = BottomSheetDialog(this, R.style.BottomSheetDialog) expireTimeDialog = BottomSheetDialog(this, R.style.BottomSheetDialog)
dialog.setContentView(dialogBinding.root) expireTimeDialog?.setContentView(dialogBinding.root)
// 设置选中状态 // 设置选中状态
when (selectedExpireOption) { when (selectedExpireOption) {
@@ -87,7 +104,7 @@ class InvitationCodeActivity : AppCompatActivity() {
} }
dialogBinding.tvCancel.setOnClickListener { dialogBinding.tvCancel.setOnClickListener {
dialog.dismiss() expireTimeDialog?.dismiss()
} }
dialogBinding.tvConfirm.setOnClickListener { dialogBinding.tvConfirm.setOnClickListener {
@@ -104,10 +121,10 @@ class InvitationCodeActivity : AppCompatActivity() {
// 选择后显示二维码并生成 // 选择后显示二维码并生成
showQrCode() showQrCode()
generateQRCode() generateQRCode()
dialog.dismiss() expireTimeDialog?.dismiss()
} }
dialog.show() expireTimeDialog?.show()
} }
private fun hideQrCode() { private fun hideQrCode() {
@@ -133,8 +150,10 @@ class InvitationCodeActivity : AppCompatActivity() {
private fun generateInviteCode(): String { private fun generateInviteCode(): String {
// 生成包含邀请信息的JSON字符串 // 生成包含邀请信息的JSON字符串
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
val expireTime = timestamp + expireTimeInMillis expireTimestamp = timestamp + expireTimeInMillis
return "{\"type\":\"invite\",\"code\":\"INV${timestamp}\",\"expire\":$expireTime}" generatedInviteCode = "INV${timestamp}"
qrCodeContent = "{\"type\":\"invite\",\"code\":\"$generatedInviteCode\",\"expire\":$expireTimestamp}"
return qrCodeContent
} }
private fun createQRCodeWithLogo(content: String, width: Int, height: Int): Bitmap? { private fun createQRCodeWithLogo(content: String, width: Int, height: Int): Bitmap? {
@@ -142,6 +161,8 @@ class InvitationCodeActivity : AppCompatActivity() {
val hints = Hashtable<EncodeHintType, Any>() val hints = Hashtable<EncodeHintType, Any>()
hints[EncodeHintType.CHARACTER_SET] = "UTF-8" hints[EncodeHintType.CHARACTER_SET] = "UTF-8"
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H
// 设置二维码边距为最小1
hints[EncodeHintType.MARGIN] = 1
val writer = QRCodeWriter() val writer = QRCodeWriter()
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints) val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints)
@@ -173,6 +194,22 @@ class InvitationCodeActivity : AppCompatActivity() {
} }
} }
// 在二维码中间绘制logo
val logoDrawable = ContextCompat.getDrawable(this, R.mipmap.loge)
if (logoDrawable != null) {
// logo大小为二维码的1/5
val logoSize = width / 5
val logoX = (width - logoSize) / 2
val logoY = (height - logoSize) / 2
val logoBitmap = Bitmap.createBitmap(logoSize, logoSize, Bitmap.Config.ARGB_8888)
val logoCanvas = Canvas(logoBitmap)
logoDrawable.setBounds(0, 0, logoSize, logoSize)
logoDrawable.draw(logoCanvas)
canvas.drawBitmap(logoBitmap, logoX.toFloat(), logoY.toFloat(), null)
}
qrBitmap qrBitmap
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -181,11 +218,160 @@ class InvitationCodeActivity : AppCompatActivity() {
} }
private fun shareToWeChat() { private fun shareToWeChat() {
// 微信分享实现 // 检查二维码是否过期
// 这里需要集成微信SDK if (expireTimestamp == 0L || System.currentTimeMillis() > expireTimestamp) {
ToastUtils.showError(this, getString(R.string.qr_code_expired))
return
}
// 生成分享海报
val shareBitmap = generateSharePoster()
if (shareBitmap == null) {
ToastUtils.showError(this, getString(R.string.share_failed))
return
}
// 保存图片到本地
val imagePath = saveBitmapToFile(shareBitmap)
if (imagePath.isEmpty()) {
ToastUtils.showError(this, getString(R.string.share_failed))
return
}
// 使用系统分享功能
shareImage(imagePath)
}
private fun generateSharePoster(): Bitmap? {
return try {
// 获取空海报背景图
val posterBackground = ContextCompat.getDrawable(this, R.drawable.share_poster_empty)
?: return null
// 创建画布
val width = posterBackground.intrinsicWidth
val height = posterBackground.intrinsicHeight
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
// 绘制背景图
posterBackground.setBounds(0, 0, width, height)
posterBackground.draw(canvas)
// 使用已生成的二维码内容生成二维码
val qrCode = createQRCodeWithLogo(qrCodeContent, 300, 300)
?: return bitmap
// 绘制二维码到海报上
val qrSize = (width * 0.38f).toInt()
val qrX = (width - qrSize) / 2
val qrY = (height * 0.60f).toInt()
val scaledQr = Bitmap.createScaledBitmap(qrCode, qrSize, qrSize, true)
canvas.drawBitmap(scaledQr, qrX.toFloat(), qrY.toFloat(), null)
// 获取时效文本
val expireText = if (expireTimeInMillis == EXPIRE_24H) {
getString(R.string.expire_24h)
} else {
getString(R.string.expire_7d)
}
// 绘制底部提示文字
val tipsText = getString(R.string.share_poster_tips, expireText)
val tipsPaint = android.text.TextPaint().apply {
color = Color.parseColor("#0475F3")
textSize = 36f
isAntiAlias = true
textAlign = Paint.Align.CENTER
isFakeBoldText = false
}
val horizontalPadding = (width * 0.12f).toInt()
val textWidth = width - horizontalPadding * 2 - (width * 0.08f).toInt()
val staticLayout = android.text.StaticLayout.Builder.obtain(
tipsText, 0, tipsText.length, tipsPaint, textWidth
)
.setAlignment(android.text.Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(0f, 1.4f)
.build()
// 计算文字位置:距离二维码底部有固定间距
val qrBottom = qrY + qrSize
val marginBelowQr = (height * 0.04f).toInt() // 二维码下方间距
val textY = qrBottom + marginBelowQr
// 向右偏移
val rightOffset = (width * 0.35f).toInt()
canvas.save()
canvas.translate((horizontalPadding + rightOffset).toFloat(), textY.toFloat())
staticLayout.draw(canvas)
canvas.restore()
bitmap
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun saveBitmapToFile(bitmap: Bitmap): String {
return try {
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
?: return ""
val file = File(storageDir, "share_poster_${System.currentTimeMillis()}.png")
val fos = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
fos.close()
file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
""
}
}
private fun shareImage(imagePath: String) {
val file = File(imagePath)
if (!file.exists()) {
ToastUtils.showError(this, getString(R.string.share_failed))
return
}
// 使用 FileProvider 获取内容 URI
val authority = "${packageName}.fileprovider"
val contentUri = androidx.core.content.FileProvider.getUriForFile(this, authority, file)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/*"
putExtra(Intent.EXTRA_STREAM, contentUri)
putExtra(Intent.EXTRA_TEXT, getString(R.string.invite_friend))
// 授予读取权限
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// 为所有匹配的应用授予权限
val resInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
grantUriPermission(packageName, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val chooser = Intent.createChooser(intent, getString(R.string.share_to))
if (intent.resolveActivity(packageManager) != null) {
startActivity(chooser)
} else {
ToastUtils.showError(this, getString(R.string.no_share_app))
}
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// 关闭弹窗,防止内存泄漏
expireTimeDialog?.dismiss()
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

View File

@@ -410,4 +410,10 @@
<string name="wechat_friend">微信好友</string> <string name="wechat_friend">微信好友</string>
<string name="expire_24h">24小时</string> <string name="expire_24h">24小时</string>
<string name="expire_7d">7天</string> <string name="expire_7d">7天</string>
<string name="qr_code_expired">二维码已过期,请重新生成</string>
<string name="share_failed">分享失败</string>
<string name="share_poster_tips">邀请二维码有效期%1$s内有效分享只能被接受1次</string>
<string name="invite_friend">快来加入我扫码下载APP</string>
<string name="share_to">分享至</string>
<string name="no_share_app">暂无可用分享应用</string>
</resources> </resources>

View File

@@ -414,4 +414,10 @@
<string name="wechat_friend">WeChat Friend</string> <string name="wechat_friend">WeChat Friend</string>
<string name="expire_24h">24h</string> <string name="expire_24h">24h</string>
<string name="expire_7d">7d</string> <string name="expire_7d">7d</string>
<string name="qr_code_expired">QR code has expired, please regenerate</string>
<string name="share_failed">Share failed</string>
<string name="share_poster_tips">The invitation QR code is valid within %1$s and can only be accepted once</string>
<string name="invite_friend">Come and join me! Scan the QR code to download the app</string>
<string name="share_to">Share to</string>
<string name="no_share_app">No sharing application available</string>
</resources> </resources>