二维码
This commit is contained in:
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<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">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Administrator\.android\avd\Pixel_10_Pro.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=AMRF025926116461" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
@@ -130,6 +130,16 @@
|
||||
<activity
|
||||
android:name=".login.LoginActivity"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.example.defenseapplication.personal
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
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.View
|
||||
import android.widget.RadioButton
|
||||
@@ -21,6 +25,8 @@ import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import com.gyf.immersionbar.ImmersionBar
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Hashtable
|
||||
|
||||
//邀请码页面
|
||||
@@ -29,6 +35,11 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
private lateinit var binding: InvitationCodeActivityBinding
|
||||
private var expireTimeInMillis: Long = 24 * 60 * 60 * 1000 // 默认24小时
|
||||
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 {
|
||||
const val EXPIRE_24H = 24 * 60 * 60 * 1000L
|
||||
@@ -62,6 +73,12 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
binding.llWechatShare.setOnClickListener {
|
||||
// 防止多次点击(间隔1秒)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastClickTime < 3000) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
lastClickTime = currentTime
|
||||
// 微信分享功能
|
||||
shareToWeChat()
|
||||
}
|
||||
@@ -70,8 +87,8 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
private fun showExpireTimeDialog() {
|
||||
val dialogBinding = DialogExpireTimeBinding.inflate(LayoutInflater.from(this))
|
||||
|
||||
val dialog = BottomSheetDialog(this, R.style.BottomSheetDialog)
|
||||
dialog.setContentView(dialogBinding.root)
|
||||
expireTimeDialog = BottomSheetDialog(this, R.style.BottomSheetDialog)
|
||||
expireTimeDialog?.setContentView(dialogBinding.root)
|
||||
|
||||
// 设置选中状态
|
||||
when (selectedExpireOption) {
|
||||
@@ -87,7 +104,7 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
dialogBinding.tvCancel.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
expireTimeDialog?.dismiss()
|
||||
}
|
||||
|
||||
dialogBinding.tvConfirm.setOnClickListener {
|
||||
@@ -104,10 +121,10 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
// 选择后显示二维码并生成
|
||||
showQrCode()
|
||||
generateQRCode()
|
||||
dialog.dismiss()
|
||||
expireTimeDialog?.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
expireTimeDialog?.show()
|
||||
}
|
||||
|
||||
private fun hideQrCode() {
|
||||
@@ -133,8 +150,10 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
private fun generateInviteCode(): String {
|
||||
// 生成包含邀请信息的JSON字符串
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val expireTime = timestamp + expireTimeInMillis
|
||||
return "{\"type\":\"invite\",\"code\":\"INV${timestamp}\",\"expire\":$expireTime}"
|
||||
expireTimestamp = timestamp + expireTimeInMillis
|
||||
generatedInviteCode = "INV${timestamp}"
|
||||
qrCodeContent = "{\"type\":\"invite\",\"code\":\"$generatedInviteCode\",\"expire\":$expireTimestamp}"
|
||||
return qrCodeContent
|
||||
}
|
||||
|
||||
private fun createQRCodeWithLogo(content: String, width: Int, height: Int): Bitmap? {
|
||||
@@ -142,6 +161,8 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
val hints = Hashtable<EncodeHintType, Any>()
|
||||
hints[EncodeHintType.CHARACTER_SET] = "UTF-8"
|
||||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H
|
||||
// 设置二维码边距为最小(1)
|
||||
hints[EncodeHintType.MARGIN] = 1
|
||||
|
||||
val writer = QRCodeWriter()
|
||||
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
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -181,11 +218,160 @@ class InvitationCodeActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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() {
|
||||
super.onDestroy()
|
||||
// 关闭弹窗,防止内存泄漏
|
||||
expireTimeDialog?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable/share_poster_empty.png
Normal file
BIN
app/src/main/res/drawable/share_poster_empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 466 KiB |
@@ -410,4 +410,10 @@
|
||||
<string name="wechat_friend">微信好友</string>
|
||||
<string name="expire_24h">24小时</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>
|
||||
@@ -414,4 +414,10 @@
|
||||
<string name="wechat_friend">WeChat Friend</string>
|
||||
<string name="expire_24h">24h</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>
|
||||
Reference in New Issue
Block a user