@@ -58,15 +58,17 @@ private object AdaptiveIconConfig {
5858 const val BACKGROUND_FILE_NAME = " morphe_adaptive_background_custom.png"
5959 const val FOREGROUND_FILE_NAME = " morphe_adaptive_foreground_custom.png"
6060
61- // Density folders and sizes
62- val DENSITY_CONFIGS = mapOf (
63- 160 to (" mipmap-mdpi" to 108 ),
64- 240 to (" mipmap-hdpi" to 162 ),
65- 320 to (" mipmap-xhdpi" to 216 ),
66- 480 to (" mipmap-xxhdpi" to 324 ),
67- Int . MAX_VALUE to (" mipmap-xxxhdpi" to 432 )
61+ // Density folders and sizes - all densities will be generated
62+ val DENSITY_CONFIGS = listOf (
63+ DensityConfig (" mipmap-mdpi" , 108 ),
64+ DensityConfig (" mipmap-hdpi" , 162 ),
65+ DensityConfig (" mipmap-xhdpi" , 216 ),
66+ DensityConfig (" mipmap-xxhdpi" , 324 ),
67+ DensityConfig (" mipmap-xxxhdpi" , 432 )
6868 )
6969
70+ data class DensityConfig (val folderName : String , val size : Int )
71+
7072 // Transform constraints
7173 const val MIN_SCALE = 0.5f
7274 const val MAX_SCALE = 3.0f
@@ -154,7 +156,7 @@ fun AdaptiveIconCreatorDialog(
154156 uri?.let {
155157 scope.launch(Dispatchers .IO ) {
156158 try {
157- val success = createAdaptiveIcon (
159+ val success = createAdaptiveIcons (
158160 context = context,
159161 baseUri = it,
160162 foregroundBitmap = foregroundBitmap!! ,
@@ -571,11 +573,11 @@ private fun SafeZoneLegendItem(
571573}
572574
573575/* *
574- * Create adaptive icon files in proper structure
576+ * Create adaptive icon files for all densities in proper structure
575577 * Returns the path to morphe_icons folder or null if failed
576578 */
577579@SuppressLint(" UseKtx" )
578- private suspend fun createAdaptiveIcon (
580+ private suspend fun createAdaptiveIcons (
579581 context : Context ,
580582 baseUri : Uri ,
581583 foregroundBitmap : Bitmap ,
@@ -585,111 +587,137 @@ private suspend fun createAdaptiveIcon(
585587 offsetY : Float
586588): String? = withContext(Dispatchers .IO ) {
587589 try {
588- // Get current device density
589- val displayMetrics = context.resources.displayMetrics
590- val density = displayMetrics.densityDpi
591-
592- // Determine which folder to create based on density
593- val (folderName, targetSize) = AdaptiveIconConfig .DENSITY_CONFIGS
594- .entries
595- .firstOrNull { density <= it.key }
596- ?.value
597- ? : AdaptiveIconConfig .DENSITY_CONFIGS [Int .MAX_VALUE ]!!
598-
599590 // Convert URI to File path using existing utility
600591 val basePath = baseUri.toFilePath()
601592 val baseDir = File (basePath)
602593
603- // Create directory structure: morphe_branding/morphe_icons/mipmap-xxx
594+ // Create directory structure: morphe_branding/morphe_icons
604595 val brandingDir = File (baseDir, AdaptiveIconConfig .BRANDING_FOLDER_NAME )
605596 if (! brandingDir.exists()) brandingDir.mkdirs()
606597
598+ // Create .nomedia file to prevent icons from appearing in gallery
599+ val nomediaFile = File (brandingDir, " .nomedia" )
600+ if (! nomediaFile.exists()) {
601+ nomediaFile.createNewFile()
602+ }
603+
607604 val iconsDir = File (brandingDir, AdaptiveIconConfig .ICONS_FOLDER_NAME )
608605 if (! iconsDir.exists()) iconsDir.mkdirs()
609606
610- val mipmapDir = File (iconsDir, folderName)
611- if (! mipmapDir.exists()) mipmapDir.mkdirs()
612-
613- // Create background bitmap (solid color)
614- val backgroundBitmap = createBitmap(targetSize, targetSize)
615- val canvas = Canvas (backgroundBitmap)
616- val rgb = parseColorToRgb(backgroundColor)
617- val paint = Paint ().apply {
618- color = android.graphics.Color .rgb(
619- (rgb.first * 255 ).toInt(),
620- (rgb.second * 255 ).toInt(),
621- (rgb.third * 255 ).toInt()
607+ // Get preview density for offset calculations
608+ val previewDensity = context.resources.displayMetrics.density
609+
610+ // Create icons for all densities
611+ AdaptiveIconConfig .DENSITY_CONFIGS .forEach { densityConfig ->
612+ createIconsForDensity(
613+ iconsDir = iconsDir,
614+ densityConfig = densityConfig,
615+ foregroundBitmap = foregroundBitmap,
616+ backgroundColor = backgroundColor,
617+ scale = scale,
618+ offsetX = offsetX,
619+ offsetY = offsetY,
620+ previewDensity = previewDensity
622621 )
623622 }
624- canvas.drawRect(0f , 0f , targetSize.toFloat(), targetSize.toFloat(), paint)
625-
626- // Create foreground bitmap with scaling and offset
627- val foregroundScaled = createBitmap(targetSize, targetSize)
628- val foregroundCanvas = Canvas (foregroundScaled)
629623
630- // Get density to convert preview offsets to target offsets
631- val previewDensity = context.resources.displayMetrics.density
624+ // Return path to 'morphe_icons' folder
625+ iconsDir.absolutePath
626+ } catch (e: Exception ) {
627+ e.printStackTrace()
628+ null
629+ }
630+ }
632631
633- // Preview canvas size in pixels
634- val previewCanvasSize = targetSize * previewDensity
632+ /* *
633+ * Create icon files for a specific density
634+ */
635+ private fun createIconsForDensity (
636+ iconsDir : File ,
637+ densityConfig : AdaptiveIconConfig .DensityConfig ,
638+ foregroundBitmap : Bitmap ,
639+ backgroundColor : String ,
640+ scale : Float ,
641+ offsetX : Float ,
642+ offsetY : Float ,
643+ previewDensity : Float
644+ ) {
645+ val targetSize = densityConfig.size
646+
647+ // Create mipmap directory
648+ val mipmapDir = File (iconsDir, densityConfig.folderName)
649+ if (! mipmapDir.exists()) mipmapDir.mkdirs()
650+
651+ // Create background bitmap (solid color)
652+ val backgroundBitmap = createBitmap(targetSize, targetSize)
653+ val canvas = Canvas (backgroundBitmap)
654+ val rgb = parseColorToRgb(backgroundColor)
655+ val paint = Paint ().apply {
656+ color = android.graphics.Color .rgb(
657+ (rgb.first * 255 ).toInt(),
658+ (rgb.second * 255 ).toInt(),
659+ (rgb.third * 255 ).toInt()
660+ )
661+ }
662+ canvas.drawRect(0f , 0f , targetSize.toFloat(), targetSize.toFloat(), paint)
635663
636- // Calculate base size by fitting image to canvas (same logic as preview)
637- val imageAspect = foregroundBitmap.width.toFloat() / foregroundBitmap.height.toFloat( )
638- val canvasAspect = previewCanvasSize / previewCanvasSize // 1.0 for square
664+ // Create foreground bitmap with scaling and offset
665+ val foregroundScaled = createBitmap(targetSize, targetSize )
666+ val foregroundCanvas = Canvas (foregroundScaled)
639667
640- val (baseWidth, baseHeight) = if (imageAspect > canvasAspect) {
641- // Image is wider - fit to width
642- previewCanvasSize to (previewCanvasSize / imageAspect)
643- } else {
644- // Image is taller - fit to height
645- (previewCanvasSize * imageAspect) to previewCanvasSize
646- }
668+ // Preview canvas size in pixels
669+ val previewCanvasSize = AdaptiveIconConfig .PREVIEW_SIZE .value * previewDensity
647670
648- // Apply user scale to the fitted size
649- val scaledWidth = baseWidth * scale
650- val scaledHeight = baseHeight * scale
671+ // Calculate base size by fitting image to canvas (same logic as preview)
672+ val imageAspect = foregroundBitmap.width.toFloat() / foregroundBitmap.height.toFloat()
673+ val canvasAspect = 1.0f // Square canvas
651674
652- // Convert to target bitmap coordinates
653- val targetScaledWidth = scaledWidth / previewDensity
654- val targetScaledHeight = scaledHeight / previewDensity
675+ val (baseWidth, baseHeight) = if (imageAspect > canvasAspect) {
676+ // Image is wider - fit to width
677+ previewCanvasSize to (previewCanvasSize / imageAspect)
678+ } else {
679+ // Image is taller - fit to height
680+ (previewCanvasSize * imageAspect) to previewCanvasSize
681+ }
655682
656- // Convert offsets from preview canvas pixels to target bitmap pixels
657- val targetOffsetX = offsetX / previewDensity
658- val targetOffsetY = offsetY / previewDensity
683+ // Apply user scale to the fitted size
684+ val scaledWidth = baseWidth * scale
685+ val scaledHeight = baseHeight * scale
659686
660- val left = (targetSize - targetScaledWidth) / 2 + targetOffsetX
661- val top = (targetSize - targetScaledHeight) / 2 + targetOffsetY
687+ // Convert to target bitmap coordinates
688+ val targetScaledWidth = scaledWidth * (targetSize / previewCanvasSize)
689+ val targetScaledHeight = scaledHeight * (targetSize / previewCanvasSize)
662690
663- // Create Paint with anti-aliasing and bicubic filtering for high-quality scaling
664- val bitmapPaint = Paint ().apply {
665- isAntiAlias = true
666- isFilterBitmap = true
667- isDither = true
668- }
691+ // Convert offsets from preview canvas pixels to target bitmap pixels
692+ val targetOffsetX = offsetX * (targetSize / previewCanvasSize)
693+ val targetOffsetY = offsetY * (targetSize / previewCanvasSize)
669694
670- val destRect = RectF (left, top, left + targetScaledWidth, top + targetScaledHeight)
671- foregroundCanvas.drawBitmap(foregroundBitmap, null , destRect, bitmapPaint)
695+ val left = (targetSize - targetScaledWidth) / 2 + targetOffsetX
696+ val top = (targetSize - targetScaledHeight) / 2 + targetOffsetY
672697
673- // Save background
674- val backgroundFile = File (mipmapDir, AdaptiveIconConfig .BACKGROUND_FILE_NAME )
675- FileOutputStream (backgroundFile).use { out ->
676- backgroundBitmap.compress(Bitmap .CompressFormat .PNG , 100 , out )
677- }
698+ // Create Paint with anti-aliasing and bicubic filtering for high-quality scaling
699+ val bitmapPaint = Paint ().apply {
700+ isAntiAlias = true
701+ isFilterBitmap = true
702+ isDither = true
703+ }
678704
679- // Save foreground
680- val foregroundFile = File (mipmapDir, AdaptiveIconConfig .FOREGROUND_FILE_NAME )
681- FileOutputStream (foregroundFile).use { out ->
682- foregroundScaled.compress(Bitmap .CompressFormat .PNG , 100 , out )
683- }
705+ val destRect = RectF (left, top, left + targetScaledWidth, top + targetScaledHeight)
706+ foregroundCanvas.drawBitmap(foregroundBitmap, null , destRect, bitmapPaint)
684707
685- // Clean up
686- backgroundBitmap.recycle()
687- foregroundScaled.recycle()
708+ // Save background
709+ val backgroundFile = File (mipmapDir, AdaptiveIconConfig .BACKGROUND_FILE_NAME )
710+ FileOutputStream (backgroundFile).use { out ->
711+ backgroundBitmap.compress(Bitmap .CompressFormat .PNG , 100 , out )
712+ }
688713
689- // Return path to 'morphe_icons' folder
690- iconsDir.absolutePath
691- } catch (e: Exception ) {
692- e.printStackTrace()
693- null
714+ // Save foreground
715+ val foregroundFile = File (mipmapDir, AdaptiveIconConfig .FOREGROUND_FILE_NAME )
716+ FileOutputStream (foregroundFile).use { out ->
717+ foregroundScaled.compress(Bitmap .CompressFormat .PNG , 100 , out )
694718 }
719+
720+ // Clean up
721+ backgroundBitmap.recycle()
722+ foregroundScaled.recycle()
695723}
0 commit comments