From 6d9024a580473dcfa958620ffbc3bf43946159ec Mon Sep 17 00:00:00 2001 From: partisan Date: Sat, 17 May 2025 20:43:06 +0200 Subject: [PATCH] General fixes, UI cleanup and popup animations --- .../weforge/xyz/pulse/AboutFragment.kt | 7 + .../xyz/pulse/CallRedirectionService.kt | 9 +- .../weforge/xyz/pulse/ContactsFragment.kt | 21 +++ .../weforge/xyz/pulse/MainActivity.kt | 43 ++++- .../weforge/xyz/pulse/MainFragment.kt | 5 + .../weforge/xyz/pulse/MatrixRainView.kt | 48 ++++++ .../xyz/pulse/PopupSettingsFragment.kt | 51 +++++- .../partisan/weforge/xyz/pulse/PopupWindow.kt | 153 +++++++++++++++++- .../partisan/weforge/xyz/pulse/Preferences.kt | 25 +++ .../weforge/xyz/pulse/ServicesFragment.kt | 8 + app/src/main/res/layout/activity_main.xml | 4 +- .../res/layout/fragment_popup_settings.xml | 76 ++++----- .../res/layout/fragment_service_settings.xml | 26 +-- app/src/main/res/layout/popup.xml | 3 + app/src/main/res/layout/switch_item.xml | 7 + app/src/main/res/menu/main_menu.xml | 12 +- app/src/main/res/menu/topbar_toggle.xml | 7 + app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values/strings.xml | 25 ++- app/src/main/res/values/styles.xml | 3 + 20 files changed, 457 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt create mode 100644 app/src/main/res/layout/switch_item.xml create mode 100644 app/src/main/res/menu/topbar_toggle.xml diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt index 48ec0a1..e312052 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/AboutFragment.kt @@ -25,6 +25,13 @@ class AboutFragment : Fragment() { return binding.root } + override fun onResume() { + super.onResume() + (requireActivity() as? MainActivity)?.setAppBarTitle( + getString(R.string.about_name) + ) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt b/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt index 105157b..5f7a9b3 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/CallRedirectionService.kt @@ -63,9 +63,16 @@ class CallRedirectionService : CallRedirectionService() { return } + val phoneNumber = handle.schemeSpecificPart + + if (prefs.isBlacklistEnabled && !prefs.isContactWhitelisted(phoneNumber)) { + placeCallUnmodified() + return + } + val records: Array try { - records = getRecordsFromPhoneNumber(handle.schemeSpecificPart) + records = getRecordsFromPhoneNumber(phoneNumber) } catch (exc: SecurityException) { placeCallUnmodified() return diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt index d7d9822..2244501 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/ContactsFragment.kt @@ -26,6 +26,27 @@ class ContactsFragment : Fragment() { return binding.root } + override fun onResume() { + super.onResume() + (requireActivity() as? MainActivity)?.apply { + setAppBarTitle(getString(R.string.settings_name), getString(R.string.whitelist_name)) + setupPopupToggle(true, prefs.isBlacklistEnabled) { isChecked -> + prefs.isBlacklistEnabled = isChecked + binding.contactRecycler.isEnabled = isChecked + binding.contactRecycler.alpha = if (isChecked) 1f else 0.4f + } + } + + // Initial state + binding.contactRecycler.isEnabled = prefs.isBlacklistEnabled + binding.contactRecycler.alpha = if (prefs.isBlacklistEnabled) 1f else 0.4f + } + + override fun onPause() { + super.onPause() + (requireActivity() as? MainActivity)?.setupPopupToggle(false) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt index 8a281be..a291b4e 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt @@ -2,6 +2,9 @@ package partisan.weforge.xyz.pulse import android.content.Intent import android.os.Bundle +import android.view.View +import android.view.Menu +import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import partisan.weforge.xyz.pulse.databinding.ActivityMainBinding import androidx.appcompat.app.ActionBarDrawerToggle @@ -9,17 +12,24 @@ import partisan.weforge.xyz.pulse.REQUIRED_PERMISSIONS import partisan.weforge.xyz.pulse.hasCallRedirectionRole import partisan.weforge.xyz.pulse.hasDrawOverlays import partisan.weforge.xyz.pulse.hasGeneralPermissions +import com.google.android.material.switchmaterial.SwitchMaterial class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var prefs: Preferences + private var popupSwitch: SwitchMaterial? = null + private var popupMenuItem: MenuItem? = null + + val popupToggle: SwitchMaterial + get() = findViewById(R.id.globalPopupToggle) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - + prefs = Preferences(this) setSupportActionBar(binding.topAppBar) val drawerToggle = ActionBarDrawerToggle( @@ -27,7 +37,7 @@ class MainActivity : AppCompatActivity() { binding.drawerLayout, binding.topAppBar, R.string.navigation_drawer_open, - R.string.navigation_drawer_close + R.string.navigation_drawer_open // The "close" string is never actually shown in the UI, so I reuse "navigation_drawer_open" as sort of a placeholder ) binding.drawerLayout.addDrawerListener(drawerToggle) drawerToggle.syncState() @@ -36,6 +46,8 @@ class MainActivity : AppCompatActivity() { .replace(R.id.fragmentContainer, MainFragment()) .commit() + setupPopupToggle(false) + binding.navigationView.setNavigationItemSelectedListener { item -> when (item.itemId) { R.id.action_popup_settings -> { @@ -73,6 +85,33 @@ class MainActivity : AppCompatActivity() { } } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.topbar_toggle, menu) + popupMenuItem = menu.findItem(R.id.globalPopupToggle) + popupSwitch = popupMenuItem?.actionView?.findViewById(R.id.globalPopupToggle) + popupMenuItem?.isVisible = false // hide by default + return true + } + + fun setupPopupToggle( + visible: Boolean, + initialState: Boolean = false, + onToggle: ((Boolean) -> Unit)? = null + ) { + popupMenuItem?.isVisible = visible + popupSwitch?.apply { + setOnCheckedChangeListener(null) + isChecked = initialState + setOnCheckedChangeListener { _, isChecked -> + onToggle?.invoke(isChecked) + } + } + } + + fun setAppBarTitle(vararg parts: String) { + binding.topAppBar.title = parts.joinToString(" > ") + } + private fun hasPermissions(): Boolean { return hasGeneralPermissions(this) && hasDrawOverlays(this) && diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt index 90b53ca..9e8c3b4 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt @@ -18,6 +18,11 @@ class MainFragment : Fragment() { private lateinit var prefs: Preferences private var lastConfettiTime = 0L + override fun onResume() { + super.onResume() + (requireActivity() as? MainActivity)?.setAppBarTitle(getString(R.string.app_name)) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt new file mode 100644 index 0000000..01b61c4 --- /dev/null +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MatrixRainView.kt @@ -0,0 +1,48 @@ +package partisan.weforge.xyz.pulse + +import android.content.Context +import android.graphics.* +import android.view.View +import kotlin.random.Random + +class MatrixRainView(context: Context) : View(context) { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.GREEN + textSize = 5f * resources.displayMetrics.density + typeface = Typeface.MONOSPACE + } + + private val charset = "01アイウエオカキクケコ".toCharArray() + private val random = Random + private var columns = 0 + private lateinit var yOffsets: IntArray + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + columns = w / paint.textSize.toInt() + yOffsets = IntArray(columns) { random.nextInt(h) } + } + + override fun onDraw(canvas: Canvas) { + // drawColor with transparent clear instead of black + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + for (i in 0 until columns) { + val x = i * paint.textSize + val y = yOffsets[i].toFloat() + val char = charset[random.nextInt(charset.size)] + + paint.alpha = 255 + canvas.drawText(char.toString(), x, y, paint) + + paint.alpha = 100 + canvas.drawText(char.toString(), x, y - paint.textSize, paint) + + yOffsets[i] += paint.textSize.toInt() + if (yOffsets[i] > height) { + yOffsets[i] = 0 + } + } + postInvalidateDelayed(50) + } +} diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt index 6bd8d97..29eda70 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt @@ -7,6 +7,10 @@ import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.AdapterView +import android.widget.CompoundButton +import android.widget.Spinner import androidx.fragment.app.Fragment import androidx.core.content.getSystemService import partisan.weforge.xyz.pulse.databinding.FragmentPopupSettingsBinding @@ -28,20 +32,35 @@ class PopupSettingsFragment : Fragment() { return binding.root } + override fun onResume() { + super.onResume() + (requireActivity() as? MainActivity)?.apply { + setAppBarTitle(getString(R.string.settings_name), getString(R.string.popup_name)) + setupPopupToggle(true, prefs.popupEnabled) { isChecked -> + prefs.popupEnabled = isChecked + updateControls(isChecked) + } + } + } + + override fun onPause() { + super.onPause() + (requireActivity() as? MainActivity)?.setupPopupToggle(false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prefs = Preferences(requireContext()) window = PopupWindow(requireContext(), null) - binding.popupEnabledCheckbox.isChecked = prefs.popupEnabled - binding.popupEnabledCheckbox.setOnCheckedChangeListener { _, isChecked -> - prefs.popupEnabled = isChecked - updateControls(isChecked) - } - binding.popupPreview.setOnClickListener { - window.preview() + window.preview(false) + } + binding.popupPreview.setOnLongClickListener { + window.preview(true) + true } binding.redirectionDelay.value = (prefs.redirectionDelay / 1000).toFloat() @@ -52,6 +71,22 @@ class PopupSettingsFragment : Fragment() { prefs.redirectionDelay = (value * 1000).toLong() } + val effectNames = resources.getStringArray(R.array.popup_effects) + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, effectNames) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.popupEffectSpinner.adapter = adapter + + // Select current setting + binding.popupEffectSpinner.setSelection(prefs.popupEffect.ordinal) + + binding.popupEffectSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + prefs.popupEffect = Preferences.PopupEffect.values()[position] + } + + override fun onNothingSelected(parent: AdapterView<*>) {} + } + val screenHeight = getScreenHeightPx() binding.popupHeightSlider.valueFrom = 0f binding.popupHeightSlider.valueTo = screenHeight.toFloat() @@ -67,6 +102,8 @@ class PopupSettingsFragment : Fragment() { binding.redirectionDelay.isEnabled = enabled binding.popupHeightSlider.isEnabled = enabled binding.popupPreview.isEnabled = enabled + binding.popupEffectSpinner.isEnabled = enabled + binding.popupEffectLabel.isEnabled = enabled } private fun getScreenHeightPx(): Int { diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt b/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt index fa81ca6..d3059ec 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/PopupWindow.kt @@ -5,15 +5,19 @@ import android.content.Context import android.content.Intent import android.graphics.Color import android.graphics.PixelFormat +import android.graphics.Rect import android.media.AudioManager import android.net.Uri import android.view.Gravity import android.view.LayoutInflater +import android.view.View import android.view.WindowManager import android.view.ContextThemeWrapper import android.widget.TextView import android.animation.ObjectAnimator import android.widget.ProgressBar +import android.view.animation.OvershootInterpolator +import android.view.animation.DecelerateInterpolator import androidx.annotation.RequiresPermission import androidx.core.content.res.use import com.google.android.material.card.MaterialCardView @@ -24,6 +28,8 @@ import android.util.Log import android.content.res.ColorStateList import com.google.android.material.color.DynamicColors import com.google.android.material.color.MaterialColors +import partisan.weforge.xyz.pulse.Preferences.PopupEffect +import partisan.weforge.xyz.pulse.MatrixRainView class PopupWindow( ctx: Context, @@ -46,7 +52,9 @@ class PopupWindow( height = WindowManager.LayoutParams.WRAP_CONTENT y = prefs.popupPosition } - private var timer: Timer? = null + private var currentEffect: PopupEffect = PopupEffect.NONE + private var matrixOverlay: View? = null + private var timer: Timer? = null init { view.setOnClickListener { @@ -58,16 +66,25 @@ class PopupWindow( applyResolvedColors(view) } - fun preview() { + fun preview(isLongPress: Boolean = false) { remove() layoutParams.y = prefs.popupPosition + val destinations = listOf( R.string.destination_signal, R.string.destination_telegram, R.string.destination_threema, + // Whatsapp smells ) setDescription(destinations.random()) add() + + val duration = if (isLongPress) prefs.redirectionDelay * 5 else prefs.redirectionDelay + timer?.cancel() + timer = Timer() + timer?.schedule(timerTask { + remove() + }, duration) } fun show(uri: Uri, destinationId: Int) { @@ -128,10 +145,125 @@ class PopupWindow( } } + private fun animateAppear() { + view.animate().cancel() + view.rotationX = 0f + view.scaleX = 1f + view.scaleY = 1f + view.alpha = 1f + + val effect = when (prefs.popupEffect) { + PopupEffect.RANDOM -> PopupEffect.values().filter { it != PopupEffect.RANDOM && it != PopupEffect.NONE }.random() + else -> prefs.popupEffect + } + currentEffect = effect + + when (effect) { + PopupEffect.NONE -> {} + PopupEffect.FADE -> { + view.alpha = 0f + view.animate().alpha(1f).setDuration(300).start() + } + PopupEffect.SCALE -> { + view.scaleX = 0f + view.scaleY = 0f + view.animate().scaleX(1f).scaleY(1f).setDuration(300).start() + } + PopupEffect.BOUNCE -> { + view.scaleX = 0.7f + view.scaleY = 0.7f + view.animate().scaleX(1f).scaleY(1f) + .setInterpolator(OvershootInterpolator()) + .setDuration(400).start() + } + PopupEffect.FLOP -> { + view.rotationX = 90f + view.alpha = 0f + view.animate().rotationX(0f).alpha(1f) + .setDuration(350) + .setInterpolator(DecelerateInterpolator()) + .start() + } + PopupEffect.MATRIX -> { + val rainView = MatrixRainView(themedCtx) + matrixOverlay?.let { + try { + windowManager?.removeViewImmediate(it) + } catch (_: Exception) {} + } + matrixOverlay = rainView + + val popupBounds = Rect() + view.getGlobalVisibleRect(popupBounds) + + val overlayParams = WindowManager.LayoutParams().apply { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + format = PixelFormat.TRANSLUCENT + flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + width = view.width + height = view.height + gravity = Gravity.BOTTOM + x = 0 + y = layoutParams.y + } + + try { + windowManager?.addView(rainView, overlayParams) + } catch (e: Exception) { + Log.e("MatrixRain", "Failed to add rainView", e) + return + } + + // Fade-in popup over 500ms + view.alpha = 0f + view.animate().cancel() + view.animate().alpha(1f).setDuration(500).start() + + // Remove MatrixRainView in sync + rainView.animate().alpha(0f).setDuration(500).withEndAction { + try { + windowManager?.removeView(rainView) + matrixOverlay = null + } catch (_: Exception) {} + }.start() + } + else -> {} + } + } + + private fun animateDisappear(onEnd: () -> Unit) { + when (currentEffect) { + PopupEffect.NONE -> onEnd() + PopupEffect.FADE -> view.animate().alpha(0f).setDuration(200).withEndAction(onEnd).start() + PopupEffect.SCALE, PopupEffect.BOUNCE -> view.animate().scaleX(0f).scaleY(0f).setDuration(200).withEndAction(onEnd).start() + PopupEffect.FLOP -> view.animate().rotationX(90f).alpha(0f).setDuration(200).withEndAction(onEnd).start() + PopupEffect.MATRIX -> { + view.animate().alpha(0f).setDuration(150).withEndAction(onEnd).start() + matrixOverlay?.let { overlay -> + overlay.animate().cancel() + overlay.animate().alpha(0f).setDuration(150).withEndAction { + try { + windowManager?.removeViewImmediate(overlay) + } catch (_: Exception) {} + matrixOverlay = null + }.start() + } + } + else -> onEnd() + } + } + private fun add(): Boolean { try { + // If already attached, force remove and re-add + if (view.parent != null) { + windowManager?.removeViewImmediate(view) + } + view.animate().cancel() windowManager?.addView(view, layoutParams) - } catch (exc: WindowManager.BadTokenException) { + animateAppear() + } catch (exc: Exception) { + Log.e("PopupWindow", "Failed to add popup view", exc) return false } return true @@ -139,9 +271,18 @@ class PopupWindow( private fun remove(): Boolean { try { - windowManager?.removeView(view) - } catch (_: IllegalArgumentException) { - } catch (_: WindowManager.BadTokenException) { + animateDisappear { + try { + windowManager?.removeView(view) + matrixOverlay?.let { + try { + windowManager?.removeViewImmediate(it) + } catch (_: Exception) {} + matrixOverlay = null + } + } catch (_: Exception) {} + } + } catch (_: Exception) { return false } return true diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt index d9b87a8..bad91c5 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt @@ -10,7 +10,9 @@ class Preferences(private val context: Context) { private const val REDIRECTION_DELAY = "redirection_delay" private const val POPUP_POSITION = "popup_position_y" private const val POPUP_ENABLED = "popup_enabled" + private val POPUP_EFFECT = "popup_effect" private const val BLACKLISTED_CONTACTS = "blacklisted_contacts" + private const val BLACKLIST_ENABLED = "blacklist_enabled" private val SERVICE_ORDER_KEY = "service_order" private const val DEFAULT_REDIRECTION_DELAY = 2000L @@ -32,6 +34,29 @@ class Preferences(private val context: Context) { hasDrawOverlays(context) && hasCallRedirectionRole(context) + enum class PopupEffect { + NONE, FADE, SCALE, BOUNCE, FLOP, MATRIX, RANDOM + } + + var popupEffect: PopupEffect + get() { + val name = prefs.getString(POPUP_EFFECT, PopupEffect.FADE.name) ?: PopupEffect.FADE.name + return try { + PopupEffect.valueOf(name) + } catch (_: IllegalArgumentException) { + // If invalid, fallback and clear the broken value + prefs.edit().remove(POPUP_EFFECT).apply() + PopupEffect.BOUNCE + } + } + set(value) { + prefs.edit().putString(POPUP_EFFECT, value.name).apply() + } + + var isBlacklistEnabled: Boolean + get() = prefs.getBoolean(BLACKLIST_ENABLED, false) + set(value) = prefs.edit { putBoolean(BLACKLIST_ENABLED, value) } + var popupEnabled: Boolean get() = prefs.getBoolean(POPUP_ENABLED, true) set(value) = prefs.edit { putBoolean(POPUP_ENABLED, value) } diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt index 2d82657..add151a 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/ServicesFragment.kt @@ -23,6 +23,14 @@ class ServiceSettingsFragment : Fragment() { return binding.root } + override fun onResume() { + super.onResume() + (requireActivity() as? MainActivity)?.setAppBarTitle( + getString(R.string.settings_name), + getString(R.string.services_name) + ) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 36fed6b..c0ed222 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -19,9 +19,11 @@ app:titleTextColor="?attr/colorOnSurface" app:navigationIconTint="?attr/colorOnSurface" app:title="@string/app_name" + app:titleTextAppearance="@style/Toolbar.Title.Small" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintEnd_toEndOf="parent"> + + android:padding="32dp"> + - - -