commit 1071afd9a8a93c72c1b36aaccfa446c259e56505 Author: Max Date: Thu Mar 7 21:00:40 2024 +0800 feat:首次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bd175f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +.idea diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..8801c3c --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..8d81632 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..ad2d379 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.heeeeka.card' + compileSdk 34 + + defaultConfig { + applicationId "com.heeeeka.card" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2' + implementation 'io.github.cymchad:BaseRecyclerViewAdapterHelper4:4.1.4' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/heeeeka/card/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/heeeeka/card/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..a06baf3 --- /dev/null +++ b/app/src/androidTest/java/com/heeeeka/card/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.heeeeka.card + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.heeeeka.card", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d043249 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/MainActivity.kt b/app/src/main/java/com/heeeeka/card/MainActivity.kt new file mode 100644 index 0000000..f7d321a --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/MainActivity.kt @@ -0,0 +1,107 @@ +package com.heeeeka.card + +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.heeeeka.card.support.ActivityResultLauncherCompat +import com.heeeeka.card.databinding.MainActivityBinding +import com.heeeeka.card.ui.display.CardDisplayDialog +import com.heeeeka.card.model.CardInfo +import com.heeeeka.card.support.CardEngine +import com.heeeeka.card.ui.bg.CardBgDialog +import com.heeeeka.card.ui.firework.CardFireworkDialog +import com.heeeeka.card.utils.ILog +import com.heeeeka.card.utils.toast +import com.gyf.immersionbar.ImmersionBar +import java.nio.charset.Charset + +class MainActivity : AppCompatActivity(), ILog { + + private var binding: MainActivityBinding? = null + private var cardEngine: CardEngine? = null + private val cardInfo: CardInfo = + CardInfo(0, 0, "", R.drawable.card_bg_1, emptyList(), R.drawable.card_firework_1) + + private val launcherCompat = + ActivityResultLauncherCompat(this, ActivityResultContracts.RequestMultiplePermissions()) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = MainActivityBinding.inflate(layoutInflater) + setContentView(binding?.root) + ImmersionBar.with(this).init() + initView() + initEvent() + } + + private fun initView() { + binding?.layoutRoot?.let { + it.post { + cardInfo.width = it.width + cardInfo.height = it.height + cardEngine = CardEngine(this, it.width, it.height) + } + } + } + + private fun initEvent() { + binding?.let { binding -> + binding.ivRefresh.setOnClickListener { + refreshFirework() + } + binding.ivGenerate.setOnClickListener { + showCardDisplayDialog() + } + binding.tvChangeBg.setOnClickListener { + showBgSelectDialog() + } + binding.tvChangeFirework.setOnClickListener { + showFireworkSelectDialog() + } + } + } + + private fun showBgSelectDialog() { + CardBgDialog(this, lifecycle, cardInfo.bgResId) { + cardInfo.bgResId = it + binding?.ivCardBg?.setImageResource(it) + }.show() + } + + private fun refreshFirework() { + val view = binding?.layoutRoot ?: return + cardEngine?.let { + cardInfo.images = it.randomImage(cardInfo.fireworkResId) + it.loadImages(view, cardInfo.images, true) + } + } + + private fun showFireworkSelectDialog() { + CardFireworkDialog(this, lifecycle, cardInfo.fireworkResId) { + cardInfo.fireworkResId = it + if (cardInfo.images.isNotEmpty()) { + refreshFirework() + } + }.show() + } + + private fun showCardDisplayDialog() { + val text = binding?.etText?.text?.trim()?.toString() ?: "" + if (getTextLength(text) > 15) { + toast(R.string.card_text_length_tips) + return + } + cardInfo.text = text + CardDisplayDialog(this, cardInfo, launcherCompat).show() + } + + private fun getTextLength(text: String): Int { + if (Charset.isSupported("GBK")) { + val charset = Charset.forName("GBK") + val newText = String(text.toByteArray(charset), charset) + return newText.length + } else { + return text.length + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/model/CardInfo.kt b/app/src/main/java/com/heeeeka/card/model/CardInfo.kt new file mode 100644 index 0000000..218421e --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/model/CardInfo.kt @@ -0,0 +1,15 @@ +package com.heeeeka.card.model + + +/** + * Created by Max on 2024/3/6 15:04 + * Desc: + **/ +data class CardInfo( + var width: Int, + var height: Int, + var text: String, + var bgResId: Int, + var images: List, + var fireworkResId: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/model/ImageInfo.kt b/app/src/main/java/com/heeeeka/card/model/ImageInfo.kt new file mode 100644 index 0000000..0ed5ff3 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/model/ImageInfo.kt @@ -0,0 +1,9 @@ +package com.heeeeka.card.model + +import java.io.Serializable + +/** + * Created by Max on 2024/3/6 15:03 + * Desc: + **/ +data class ImageInfo(val resId: Int, val left: Float, val top: Float) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/support/ActivityResultLauncherCompat.kt b/app/src/main/java/com/heeeeka/card/support/ActivityResultLauncherCompat.kt new file mode 100644 index 0000000..4bf4e5d --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/support/ActivityResultLauncherCompat.kt @@ -0,0 +1,71 @@ +package com.heeeeka.card.support + +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultCaller +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner + +class ActivityResultLauncherCompat constructor( + private val caller: ActivityResultCaller, + private val contract: ActivityResultContract, + private val registry: ActivityResultRegistry?, + private val lifecycleOwner: LifecycleOwner +) : DefaultLifecycleObserver, ActivityResultCallback { + + private var activityResultLauncher: ActivityResultLauncher? = null + private var activityResultCallback: ActivityResultCallback? = null + + constructor( + caller: ActivityResultCaller, + contract: ActivityResultContract, + lifecycleOwner: LifecycleOwner + ) : this(caller, contract, null, lifecycleOwner) + + constructor(fragment: Fragment, contract: ActivityResultContract) : + this(fragment, contract, fragment) + + constructor(activity: FragmentActivity, contract: ActivityResultContract) : + this(activity, contract, activity) + + init { + lifecycleOwner.lifecycle.addObserver(this) + } + + override fun onCreate(owner: LifecycleOwner) { + activityResultLauncher = if (registry == null) { + caller.registerForActivityResult(contract, this) + } else { + caller.registerForActivityResult(contract, registry, this) + } + } + + override fun onStart(owner: LifecycleOwner) { + if (activityResultLauncher == null) { + throw IllegalStateException("ActivityResultLauncherCompat must initialize before they are STARTED.") + } + } + + override fun onDestroy(owner: LifecycleOwner) { + lifecycleOwner.lifecycle.removeObserver(this) + } + + override fun onActivityResult(result: O) { + activityResultCallback?.onActivityResult(result) + } + + fun launch(input: I, callback: ActivityResultCallback) { + launch(input, null, callback) + } + + fun launch(input: I, options: ActivityOptionsCompat?, callback: ActivityResultCallback) { + activityResultCallback = callback + activityResultLauncher?.launch(input, options) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/support/CardEngine.kt b/app/src/main/java/com/heeeeka/card/support/CardEngine.kt new file mode 100644 index 0000000..408358c --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/support/CardEngine.kt @@ -0,0 +1,107 @@ +package com.heeeeka.card.support + +import android.content.Context +import android.view.ViewGroup.MarginLayoutParams +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import com.heeeeka.card.model.ImageInfo +import com.heeeeka.card.utils.ILog +import kotlin.random.Random + +class CardEngine( + private val context: Context, + private val cardWidth: Int, + private val cardHeight: Int +) : ILog { + + private val maxImageCount = 5 + private val imageWidthPercent = 0.3 + + private val imageViewCache = ArrayList() + private val displayImageList = ArrayList() + + fun randomImage(imageResId: Int): MutableList { + val list = ArrayList() + val size = Random.nextInt(3, maxImageCount + 1) + logD("randomImage size:${size}") + repeat(size) { + val left = Random.nextDouble(1 - imageWidthPercent) + val top = Random.nextDouble(1 - (imageWidthPercent * cardWidth / cardHeight)) + logD("randomImage() left:$left top:$top") + list.add(ImageInfo(imageResId, left.toFloat(), top.toFloat())) + } + return list + } + + fun loadImages(pView: ConstraintLayout, images: List, needAnimation: Boolean) { + logD("loadImages() size:${images.size}") + displayImageList.forEach { + it.isVisible = false + } + imageViewCache.addAll(displayImageList) + displayImageList.clear() + images.forEach { + val imageView = getImageView(context) + imageView.isVisible = false + if (imageView.parent == null) { + pView.addView(imageView) + } + imageView.setImageResource(it.resId) + (imageView.layoutParams as? MarginLayoutParams)?.let { layoutParams -> + layoutParams.setMargins( + (cardWidth * it.left).toInt(), + (cardHeight * it.top).toInt(), + 0, + 0 + ) + imageView.layoutParams = layoutParams + } + showImage(imageView, needAnimation) + } + } + + private fun showImage(imageView: ImageView, needAnimation: Boolean) { + displayImageList.add(imageView) + imageView.alpha = 0f + imageView.scaleX = 0f + imageView.scaleY = 0f + imageView.isVisible = true + if (needAnimation) { + imageView.animate().alpha(1f).scaleX(1f).scaleY(1f) + .setListener(object : DefAnimListener {}) + .setStartDelay(Random.nextLong(100)).start() + } else { + imageView.alpha = 1f + imageView.scaleX = 1f + imageView.scaleY = 1f + } + } + + private fun getImageView(context: Context): ImageView { + var imageView = imageViewCache.firstOrNull() + if (imageView != null) { + imageViewCache.removeFirst() + } else { + imageView = createImageView(context) + } + return imageView + } + + private fun createImageView( + context: Context, + ): ImageView { + return ImageView(context).apply { + layoutParams = ConstraintLayout.LayoutParams(0, 0).apply { + this.dimensionRatio = "1:1" + this.matchConstraintPercentWidth = 0.3f + this.topToTop = ConstraintLayout.LayoutParams.PARENT_ID + this.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID + } + } + } + + override fun getLogTag(): String { + return "CardEngine" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/support/DefAnimListener.kt b/app/src/main/java/com/heeeeka/card/support/DefAnimListener.kt new file mode 100644 index 0000000..b595ac5 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/support/DefAnimListener.kt @@ -0,0 +1,21 @@ +package com.heeeeka.card.support + +import android.animation.Animator + +/** + * Created by Max on 2024/3/6 11:43 + * Desc: + **/ +interface DefAnimListener : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) { + } + + override fun onAnimationEnd(animation: Animator) { + } + + override fun onAnimationCancel(animation: Animator) { + } + + override fun onAnimationRepeat(animation: Animator) { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/ui/bg/CardBgAdapter.kt b/app/src/main/java/com/heeeeka/card/ui/bg/CardBgAdapter.kt new file mode 100644 index 0000000..dc8c3b1 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/ui/bg/CardBgAdapter.kt @@ -0,0 +1,53 @@ +package com.heeeeka.card.ui.bg + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.chad.library.adapter4.BaseQuickAdapter +import com.chad.library.adapter4.viewholder.QuickViewHolder +import com.heeeeka.card.R +import com.google.android.material.imageview.ShapeableImageView + +class CardBgAdapter : BaseQuickAdapter() { + + var selectItem: Int? = null + + init { + setOnItemClickListener { adapter, view, position -> + selectItem = getItem(position) + notifyItemRangeChanged(0, itemCount, true) + } + } + + override fun onBindViewHolder( + holder: QuickViewHolder, + position: Int, + item: Int?, + payloads: List + ) { + super.onBindViewHolder(holder, position, item, payloads) + onBindState(holder, position, item) + } + + override fun onBindViewHolder(holder: QuickViewHolder, position: Int, item: Int?) { + val imageView = holder.getView(R.id.iv_img) + item?.let { + imageView.setImageResource(item) + } + onBindState(holder, position, item) + } + + private fun onBindState(holder: QuickViewHolder, position: Int, item: Int?) { + val imageView = holder.getView(R.id.iv_border) + imageView.isVisible = item == selectItem + } + + override fun onCreateViewHolder( + context: Context, + parent: ViewGroup, + viewType: Int + ): QuickViewHolder { + return QuickViewHolder(R.layout.card_bg_item, parent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/ui/bg/CardBgDialog.kt b/app/src/main/java/com/heeeeka/card/ui/bg/CardBgDialog.kt new file mode 100644 index 0000000..f46556c --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/ui/bg/CardBgDialog.kt @@ -0,0 +1,75 @@ +package com.heeeeka.card.ui.bg + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding +import com.heeeeka.card.R +import com.heeeeka.card.databinding.CardBgDialogBinding +import com.heeeeka.card.utils.ILog + +/** + * Created by Max on 2024/3/6 14:14 + * Desc: + **/ +class CardBgDialog( + context: Context, + private val lifecycle: Lifecycle, + private val selectItem: Int, + private var listener: ((Int) -> Unit)? = null +) : + Dialog(context, R.style.base_dialog), LifecycleEventObserver, ILog { + + private var binding: CardBgDialogBinding? = null + + private val adapter = CardBgAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(this) + binding = CardBgDialogBinding.inflate(layoutInflater) + binding?.root?.let { setContentView(it) } + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + binding?.let { binding -> + adapter.selectItem = selectItem + binding.recyclerView.adapter = adapter + adapter.addAll(getBgList()) + binding.tvCancel.setOnClickListener { + dismiss() + } + binding.tvConfirm.setOnClickListener { + val item = adapter.selectItem ?: return@setOnClickListener + listener?.invoke(item) + dismiss() + } + } + } + + private fun getBgList(): List { + return listOf( + R.drawable.card_bg_1, + R.drawable.card_bg_2, + R.drawable.card_bg_3, + R.drawable.card_bg_4, + R.drawable.card_bg_5, + R.drawable.card_bg_6 + ) + } + + override fun dismiss() { + super.dismiss() + lifecycle.removeObserver(this) + listener = null + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_DESTROY) { + dismiss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/ui/display/CardDisplayDialog.kt b/app/src/main/java/com/heeeeka/card/ui/display/CardDisplayDialog.kt new file mode 100644 index 0000000..bb1a562 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/ui/display/CardDisplayDialog.kt @@ -0,0 +1,103 @@ +package com.heeeeka.card.ui.display + +import android.Manifest +import android.app.Dialog +import android.graphics.Bitmap +import android.os.Build +import android.os.Bundle +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.drawToBitmap +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import com.heeeeka.card.R +import com.heeeeka.card.databinding.CardDisplayDialogBinding +import com.heeeeka.card.model.CardInfo +import com.heeeeka.card.support.ActivityResultLauncherCompat +import com.heeeeka.card.support.CardEngine +import com.heeeeka.card.utils.ILog +import com.heeeeka.card.utils.UiUtils +import com.heeeeka.card.utils.roundCorner +import com.heeeeka.card.utils.toast + +class CardDisplayDialog( + private val activity: FragmentActivity, + private val cardInfo: CardInfo, + private val launcherCompat: ActivityResultLauncherCompat, Map> +) : Dialog(activity, R.style.base_dialog), LifecycleEventObserver, ILog { + + private var binding: CardDisplayDialogBinding? = null + private var cardEngine: CardEngine? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activity.lifecycle.addObserver(this) + binding = CardDisplayDialogBinding.inflate(layoutInflater) + binding?.root?.let { setContentView(it) } + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + binding?.let { binding -> + binding.layoutCard.roundCorner(UiUtils.dip2px(context, 12f)) + (binding.layoutCard.layoutParams as? ConstraintLayout.LayoutParams)?.let { + it.dimensionRatio = "${cardInfo.width}:${cardInfo.height}" + logD("dimensionRatio:${it.dimensionRatio}") + binding.layoutCard.layoutParams = it + } + binding.layoutCard.post { + cardEngine = + CardEngine(context, binding.layoutCard.width, binding.layoutCard.height) + loadCard(cardInfo) + } + binding.tvSave.setOnClickListener { + saveCard(binding.layoutCard.drawToBitmap()) + } + binding.tvCancel.setOnClickListener { + dismiss() + } + } + } + + private fun loadCard(cardInfo: CardInfo) { + binding?.layoutCard?.let { + cardEngine?.loadImages(it, cardInfo.images, false) + binding?.ivCardBg?.setImageResource(cardInfo.bgResId) + binding?.tvCardText?.text = cardInfo.text + } + } + + private fun saveCard(bitmap: Bitmap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + saveImageImpl(bitmap) + } else { + val permissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + launcherCompat.launch(permissions) { + if (it.values.firstOrNull { result -> !result } == null) { + saveImageImpl(bitmap) + } else { + context.toast(R.string.permission_external_storage_denied, true) + } + } + } + } + + private fun saveImageImpl(bitmap: Bitmap) { + logD("saveImageImpl thread:${Thread.currentThread().name}") + logD("MAAAX", "saveImageImpl end") + } + + override fun dismiss() { + super.dismiss() + activity.lifecycle.removeObserver(this) + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_DESTROY) { + dismiss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/ui/firework/CardFireworkAdapter.kt b/app/src/main/java/com/heeeeka/card/ui/firework/CardFireworkAdapter.kt new file mode 100644 index 0000000..084e04a --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/ui/firework/CardFireworkAdapter.kt @@ -0,0 +1,53 @@ +package com.heeeeka.card.ui.firework + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.chad.library.adapter4.BaseQuickAdapter +import com.chad.library.adapter4.viewholder.QuickViewHolder +import com.heeeeka.card.R +import com.google.android.material.imageview.ShapeableImageView + +class CardFireworkAdapter : BaseQuickAdapter() { + + var selectItem: Int? = null + + init { + setOnItemClickListener { adapter, view, position -> + selectItem = getItem(position) + notifyItemRangeChanged(0, itemCount, true) + } + } + + override fun onBindViewHolder( + holder: QuickViewHolder, + position: Int, + item: Int?, + payloads: List + ) { + super.onBindViewHolder(holder, position, item, payloads) + onBindState(holder, position, item) + } + + override fun onBindViewHolder(holder: QuickViewHolder, position: Int, item: Int?) { + val imageView = holder.getView(R.id.iv_img) + item?.let { + imageView.setImageResource(item) + } + onBindState(holder, position, item) + } + + private fun onBindState(holder: QuickViewHolder, position: Int, item: Int?) { + val imageView = holder.getView(R.id.iv_border) + imageView.isVisible = item == selectItem + } + + override fun onCreateViewHolder( + context: Context, + parent: ViewGroup, + viewType: Int + ): QuickViewHolder { + return QuickViewHolder(R.layout.card_firework_item, parent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/ui/firework/CardFireworkDialog.kt b/app/src/main/java/com/heeeeka/card/ui/firework/CardFireworkDialog.kt new file mode 100644 index 0000000..cfa6af0 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/ui/firework/CardFireworkDialog.kt @@ -0,0 +1,68 @@ +package com.heeeeka.card.ui.firework + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import com.heeeeka.card.R +import com.heeeeka.card.databinding.CardFireworkDialogBinding +import com.heeeeka.card.utils.ILog + +class CardFireworkDialog( + context: Context, + private val lifecycle: Lifecycle, + private val selectItem: Int, + private var listener: ((Int) -> Unit)? = null +) : + Dialog(context, R.style.base_dialog), LifecycleEventObserver, ILog { + + private var binding: CardFireworkDialogBinding? = null + + private val adapter = CardFireworkAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(this) + binding = CardFireworkDialogBinding.inflate(layoutInflater) + binding?.root?.let { setContentView(it) } + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + binding?.let { binding -> + adapter.selectItem = selectItem + binding.recyclerView.adapter = adapter + adapter.addAll(getBgList()) + binding.tvCancel.setOnClickListener { + dismiss() + } + binding.tvConfirm.setOnClickListener { + val item = adapter.selectItem ?: return@setOnClickListener + listener?.invoke(item) + dismiss() + } + } + } + + private fun getBgList(): List { + return listOf( + R.drawable.card_firework_1, + R.drawable.card_firework_2, + R.drawable.card_firework_3, + R.drawable.card_firework_4 + ) + } + + override fun dismiss() { + super.dismiss() + lifecycle.removeObserver(this) + listener = null + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_DESTROY) { + dismiss() + } + } +} diff --git a/app/src/main/java/com/heeeeka/card/utils/ILog.kt b/app/src/main/java/com/heeeeka/card/utils/ILog.kt new file mode 100644 index 0000000..8a4c594 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/utils/ILog.kt @@ -0,0 +1,21 @@ +package com.heeeeka.card.utils + +import android.util.Log + +/** + * Created by Max on 2024/3/6 15:43 + * Desc: + **/ +interface ILog { + fun logD(message: String) { + Log.d(getLogTag(), message) + } + + fun logD(tag: String, message: String) { + Log.d(tag, message) + } + + fun getLogTag(): String { + return "ILog" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/utils/ImageExt.kt b/app/src/main/java/com/heeeeka/card/utils/ImageExt.kt new file mode 100644 index 0000000..a515e53 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/utils/ImageExt.kt @@ -0,0 +1,253 @@ +@file:JvmName("ImageExt") +@file:Suppress("unused") + +package com.heeeeka.card.utils + +import android.content.* +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import java.io.File +import java.io.FileNotFoundException +import java.io.InputStream +import java.io.OutputStream + + +private const val TAG = "ImageExt" + +private val ALBUM_DIR = Environment.DIRECTORY_PICTURES + +private class OutputFileTaker(var file: File? = null) + +/** + * 复制图片文件到相册的Pictures文件夹 + * + * @param context 上下文 + * @param fileName 文件名。 需要携带后缀 + * @param relativePath 相对于Pictures的路径 + */ +fun File.copyToAlbum(context: Context, fileName: String, relativePath: String?): Uri? { + if (!this.canRead() || !this.exists()) { + Log.w(TAG, "check: read file error: $this") + return null + } + return this.inputStream().use { + it.saveToAlbum(context, fileName, relativePath) + } +} + +/** + * 保存图片Stream到相册的Pictures文件夹 + * + * @param context 上下文 + * @param fileName 文件名。 需要携带后缀 + * @param relativePath 相对于Pictures的路径 + */ +fun InputStream.saveToAlbum(context: Context, fileName: String, relativePath: String?): Uri? { + val resolver = context.contentResolver + val outputFile = OutputFileTaker() + val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile) + if (imageUri == null) { + Log.w(TAG, "insert: error: uri == null") + return null + } + + (imageUri.outputStream(resolver) ?: return null).use { output -> + this.use { input -> + input.copyTo(output) + imageUri.finishPending(context, resolver, outputFile.file) + } + } + return imageUri +} + +/** + * 保存Bitmap到相册的Pictures文件夹 + * + * https://developer.android.google.cn/training/data-storage/shared/media + * + * @param context 上下文 + * @param fileName 文件名。 需要携带后缀 + * @param relativePath 相对于Pictures的路径 + * @param quality 质量 + */ +fun Bitmap.saveToAlbum( + context: Context, + fileName: String, + relativePath: String? = null, + quality: Int = 75, +): Uri? { + // 插入图片信息 + val resolver = context.contentResolver + val outputFile = OutputFileTaker() + val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile) + if (imageUri == null) { + Log.w(TAG, "insert: error: uri == null") + return null + } + + // 保存图片 + (imageUri.outputStream(resolver) ?: return null).use { + val format = fileName.getBitmapFormat() + this@saveToAlbum.compress(format, quality, it) + imageUri.finishPending(context, resolver, outputFile.file) + } + return imageUri +} + +private fun Uri.outputStream(resolver: ContentResolver): OutputStream? { + return try { + resolver.openOutputStream(this) + } catch (e: FileNotFoundException) { + Log.e(TAG, "save: open stream error: $e") + null + } +} + +private fun Uri.finishPending( + context: Context, + resolver: ContentResolver, + outputFile: File?, +) { + val imageValues = ContentValues() + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (outputFile != null) { + imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length()) + } + resolver.update(this, imageValues, null, null) + // 通知媒体库更新 + val intent = Intent(@Suppress("DEPRECATION") Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this) + context.sendBroadcast(intent) + } else { + // Android Q添加了IS_PENDING状态,为0时其他应用才可见 + imageValues.put(MediaStore.Images.Media.IS_PENDING, 0) + resolver.update(this, imageValues, null, null) + } +} + +private fun String.getBitmapFormat(): Bitmap.CompressFormat { + val fileName = this.lowercase() + return when { + fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG + fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG + fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP + else -> Bitmap.CompressFormat.PNG + } +} + +private fun String.getMimeType(): String? { + val fileName = this.lowercase() + return when { + fileName.endsWith(".png") -> "image/png" + fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg" + fileName.endsWith(".webp") -> "image/webp" + fileName.endsWith(".gif") -> "image/gif" + else -> null + } +} + +/** + * 插入图片到媒体库 + */ +private fun ContentResolver.insertMediaImage( + fileName: String, + relativePath: String?, + outputFileTaker: OutputFileTaker? = null, +): Uri? { + // 图片信息 + val imageValues = ContentValues().apply { + val mimeType = fileName.getMimeType() + if (mimeType != null) { + put(MediaStore.Images.Media.MIME_TYPE, mimeType) + } + val date = System.currentTimeMillis() / 1000 + put(MediaStore.Images.Media.DATE_ADDED, date) + put(MediaStore.Images.Media.DATE_MODIFIED, date) + } + // 保存的位置 + val collection: Uri + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val path = if (relativePath != null) "${ALBUM_DIR}/${relativePath}" else ALBUM_DIR + imageValues.apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.RELATIVE_PATH, path) + put(MediaStore.Images.Media.IS_PENDING, 1) + } + collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + // 高版本不用查重直接插入,会自动重命名 + } else { + // 老版本 + val pictures = + @Suppress("DEPRECATION") Environment.getExternalStoragePublicDirectory(ALBUM_DIR) + val saveDir = if (relativePath != null) File(pictures, relativePath) else pictures + + if (!saveDir.exists() && !saveDir.mkdirs()) { + Log.e(TAG, "save: error: can't create Pictures directory") + return null + } + + // 文件路径查重,重复的话在文件名后拼接数字 + var imageFile = File(saveDir, fileName) + val fileNameWithoutExtension = imageFile.nameWithoutExtension + val fileExtension = imageFile.extension + + var queryUri = this.queryMediaImage28(imageFile.absolutePath) + var suffix = 1 + while (queryUri != null) { + val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension + imageFile = File(saveDir, newName) + queryUri = this.queryMediaImage28(imageFile.absolutePath) + } + + imageValues.apply { + put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name) + // 保存路径 + val imagePath = imageFile.absolutePath + Log.v(TAG, "save file: $imagePath") + put(@Suppress("DEPRECATION") MediaStore.Images.Media.DATA, imagePath) + } + outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小 + collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + // 插入图片信息 + return this.insert(collection, imageValues) +} + +/** + * Android Q以下版本,查询媒体库中当前路径是否存在 + * @return Uri 返回null时说明不存在,可以进行图片插入逻辑 + */ +private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null + + val imageFile = File(imagePath) + if (imageFile.canRead() && imageFile.exists()) { + Log.v(TAG, "query: path: $imagePath exists") + // 文件已存在,返回一个file://xxx的uri + return Uri.fromFile(imageFile) + } + // 保存的位置 + val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + + // 查询是否已经存在相同图片 + val query = this.query( + collection, + arrayOf(MediaStore.Images.Media._ID, @Suppress("DEPRECATION") MediaStore.Images.Media.DATA), + "${@Suppress("DEPRECATION") MediaStore.Images.Media.DATA} == ?", + arrayOf(imagePath), null + ) + query?.use { + while (it.moveToNext()) { + val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + val id = it.getLong(idColumn) + val existsUri = ContentUris.withAppendedId(collection, id) + Log.v(TAG, "query: path: $imagePath exists uri: $existsUri") + return existsUri + } + } + return null +} diff --git a/app/src/main/java/com/heeeeka/card/utils/ShapeViewOutlineProvider.kt b/app/src/main/java/com/heeeeka/card/utils/ShapeViewOutlineProvider.kt new file mode 100644 index 0000000..40db635 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/utils/ShapeViewOutlineProvider.kt @@ -0,0 +1,30 @@ +package com.heeeeka.card.utils + +import android.graphics.Outline +import android.view.View +import android.view.ViewOutlineProvider +import kotlin.math.min + +class ShapeViewOutlineProvider { + + class Round(var corner: Float) : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + view.width, + view.height, + corner + ) + } + } + + class Circle : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + val min = min(view.width, view.height) + val left = (view.width - min) / 2 + val top = (view.height - min) / 2 + outline.setOval(left, top, min, min) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/utils/ToastKtx.kt b/app/src/main/java/com/heeeeka/card/utils/ToastKtx.kt new file mode 100644 index 0000000..2d613a0 --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/utils/ToastKtx.kt @@ -0,0 +1,19 @@ +package com.heeeeka.card.utils + +import android.content.Context +import android.widget.Toast + + +fun Context.toast(message: Int, isLong: Boolean = false) { + toast(getString(message)) +} + +fun Context.toast(message: CharSequence, isLong: Boolean = false) { + Toast.makeText( + this, message, if (isLong) { + Toast.LENGTH_LONG + } else { + Toast.LENGTH_SHORT + } + ).show() +} \ No newline at end of file diff --git a/app/src/main/java/com/heeeeka/card/utils/UiUtils.kt b/app/src/main/java/com/heeeeka/card/utils/UiUtils.kt new file mode 100644 index 0000000..1e55fed --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/utils/UiUtils.kt @@ -0,0 +1,24 @@ +package com.heeeeka.card.utils + +import android.content.Context +import android.util.TypedValue + + +object UiUtils { + + fun dip2px(context: Context, dpValue: Float): Int { + return (TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dpValue, + context.resources.displayMetrics + ) + 0.5f).toInt() + } + + fun px2dip(context: Context, pxValue: Float): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_PX, + pxValue, + context.resources.displayMetrics + ) + } +} diff --git a/app/src/main/java/com/heeeeka/card/utils/ViewKtx.kt b/app/src/main/java/com/heeeeka/card/utils/ViewKtx.kt new file mode 100644 index 0000000..0f4005b --- /dev/null +++ b/app/src/main/java/com/heeeeka/card/utils/ViewKtx.kt @@ -0,0 +1,25 @@ +package com.heeeeka.card.utils + +import android.os.Build +import android.view.View + + +fun T.roundCorner(corner: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (outlineProvider == null || outlineProvider !is ShapeViewOutlineProvider.Round) { + outlineProvider = ShapeViewOutlineProvider.Round(corner.toFloat()) + } else if (outlineProvider != null && outlineProvider is ShapeViewOutlineProvider.Round) { + (outlineProvider as ShapeViewOutlineProvider.Round).corner = corner.toFloat() + } + clipToOutline = true + } +} + +fun T.circle() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (outlineProvider == null || outlineProvider !is ShapeViewOutlineProvider.Circle) { + outlineProvider = ShapeViewOutlineProvider.Circle() + } + clipToOutline = true + } +} diff --git a/app/src/main/res/drawable-xxhdpi/card_bg_1.webp b/app/src/main/res/drawable-xxhdpi/card_bg_1.webp new file mode 100644 index 0000000..f1348b4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_bg_1.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_bg_2.webp b/app/src/main/res/drawable-xxhdpi/card_bg_2.webp new file mode 100644 index 0000000..f9f3405 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_bg_2.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_bg_3.webp b/app/src/main/res/drawable-xxhdpi/card_bg_3.webp new file mode 100644 index 0000000..c8caf33 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_bg_3.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_bg_4.webp b/app/src/main/res/drawable-xxhdpi/card_bg_4.webp new file mode 100644 index 0000000..2cfcbbc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_bg_4.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_bg_5.webp b/app/src/main/res/drawable-xxhdpi/card_bg_5.webp new file mode 100644 index 0000000..a7f65d4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_bg_5.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_bg_6.webp b/app/src/main/res/drawable-xxhdpi/card_bg_6.webp new file mode 100644 index 0000000..2ca73e0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_bg_6.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_firework_1.webp b/app/src/main/res/drawable-xxhdpi/card_firework_1.webp new file mode 100644 index 0000000..c93b2eb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_firework_1.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_firework_2.webp b/app/src/main/res/drawable-xxhdpi/card_firework_2.webp new file mode 100644 index 0000000..2620dad Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_firework_2.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_firework_3.webp b/app/src/main/res/drawable-xxhdpi/card_firework_3.webp new file mode 100644 index 0000000..d7dbdbd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_firework_3.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/card_firework_4.webp b/app/src/main/res/drawable-xxhdpi/card_firework_4.webp new file mode 100644 index 0000000..3403053 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_firework_4.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/main_ic_generate.webp b/app/src/main/res/drawable-xxhdpi/main_ic_generate.webp new file mode 100644 index 0000000..9868a0a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_ic_generate.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/main_ic_light_fireworks.webp b/app/src/main/res/drawable-xxhdpi/main_ic_light_fireworks.webp new file mode 100644 index 0000000..aa2e8a0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/main_ic_light_fireworks.webp differ diff --git a/app/src/main/res/drawable/card_bg_select.xml b/app/src/main/res/drawable/card_bg_select.xml new file mode 100644 index 0000000..f4bcce5 --- /dev/null +++ b/app/src/main/res/drawable/card_bg_select.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_e5e3e0_8dp.xml b/app/src/main/res/drawable/shape_e5e3e0_8dp.xml new file mode 100644 index 0000000..1fb5d3d --- /dev/null +++ b/app/src/main/res/drawable/shape_e5e3e0_8dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fff_18dp.xml b/app/src/main/res/drawable/shape_fff_18dp.xml new file mode 100644 index 0000000..0a7a3a5 --- /dev/null +++ b/app/src/main/res/drawable/shape_fff_18dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_theme_btn_bg.xml b/app/src/main/res/drawable/shape_theme_btn_bg.xml new file mode 100644 index 0000000..3c17b87 --- /dev/null +++ b/app/src/main/res/drawable/shape_theme_btn_bg.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/text_bg.xml b/app/src/main/res/drawable/text_bg.xml new file mode 100644 index 0000000..c3abe3e --- /dev/null +++ b/app/src/main/res/drawable/text_bg.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_bg_dialog.xml b/app/src/main/res/layout/card_bg_dialog.xml new file mode 100644 index 0000000..8c0ed8c --- /dev/null +++ b/app/src/main/res/layout/card_bg_dialog.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_bg_item.xml b/app/src/main/res/layout/card_bg_item.xml new file mode 100644 index 0000000..8d34e25 --- /dev/null +++ b/app/src/main/res/layout/card_bg_item.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_display_dialog.xml b/app/src/main/res/layout/card_display_dialog.xml new file mode 100644 index 0000000..7605f19 --- /dev/null +++ b/app/src/main/res/layout/card_display_dialog.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_firework_dialog.xml b/app/src/main/res/layout/card_firework_dialog.xml new file mode 100644 index 0000000..dfe53de --- /dev/null +++ b/app/src/main/res/layout/card_firework_dialog.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_firework_item.xml b/app/src/main/res/layout/card_firework_item.xml new file mode 100644 index 0000000..bacc5ae --- /dev/null +++ b/app/src/main/res/layout/card_firework_item.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000..affe939 --- /dev/null +++ b/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..fa16045 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..d7bbedc Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2a8555e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..52fd715 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5c700ea --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ + + 賀卡生產器 + + 更改背景 + 更改煙花 + 輸入你的祝福語 + 選擇背景 + 確定 + 取消 + 已生成賀卡 + 保存相冊 + 選擇煙花類型 + 祝福語太長 + 存储权限被禁止,为了正常使用该功能,请前往系统设置页手动开启 + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..590d3dd --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..6d4c0ff --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/heeeeka/card/ExampleUnitTest.kt b/app/src/test/java/com/heeeeka/card/ExampleUnitTest.kt new file mode 100644 index 0000000..03b02a8 --- /dev/null +++ b/app/src/test/java/com/heeeeka/card/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.heeeeka.card + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..57f5116 --- /dev/null +++ b/build.gradle @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { +id 'com.android.application' version '8.2.1' apply false + id 'org.jetbrains.kotlin.android' version '1.9.22' apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f285de3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.injected.testOnly=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5082df8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Mar 05 19:04:54 CST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ca7fc31 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Card" +include ':app'