Render OneUI lockscreen Cards overflow grid
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
package se.ajpanton.notificationsmaster.module
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import se.ajpanton.notificationsmaster.visibility.CardsGridLayout
|
||||
import se.ajpanton.notificationsmaster.visibility.CardsGridSettings
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.WeakHashMap
|
||||
|
||||
/** Renders a OneUI Cards-mode icon grid from notification icon snapshots. */
|
||||
internal class OneUiCardsGridRenderer {
|
||||
private val states = WeakHashMap<ViewGroup, State>()
|
||||
|
||||
fun updateSources(container: ViewGroup, sources: List<Source>) {
|
||||
val unique = LinkedHashMap<String, Source>()
|
||||
sources.forEach { unique.putIfAbsent(it.key, it) }
|
||||
state(container).sources = unique.values.toList()
|
||||
}
|
||||
|
||||
fun refresh(container: ViewGroup, active: Boolean, visibleCardKeys: Set<String>, config: Config) {
|
||||
val state = state(container)
|
||||
state.active = active
|
||||
state.visibleCardKeys = visibleCardKeys
|
||||
state.config = config
|
||||
container.post { render(container, state, state.active) }
|
||||
}
|
||||
|
||||
fun refreshAll(
|
||||
active: (ViewGroup) -> Boolean,
|
||||
visibleCardKeys: (ViewGroup) -> Set<String>,
|
||||
config: Config,
|
||||
) {
|
||||
states.keys.toList().forEach { container ->
|
||||
refresh(container, active(container), visibleCardKeys(container), config)
|
||||
}
|
||||
}
|
||||
|
||||
private fun render(container: ViewGroup, state: State, active: Boolean) {
|
||||
if (!active || !container.isAttachedToWindow) {
|
||||
failOpen(state)
|
||||
return
|
||||
}
|
||||
val root = container.rootView as? ViewGroup ?: return failOpen(state)
|
||||
val remaining = state.sources.filterNot { it.key in state.visibleCardKeys }
|
||||
if (remaining.isEmpty()) {
|
||||
failOpen(state)
|
||||
return
|
||||
}
|
||||
val host = attachHost(root, container, state) ?: return failOpen(state)
|
||||
val iconSize = iconSize(root, container, state.config.settings)
|
||||
val settings = state.config.settings
|
||||
val horizontalPadding = dp(root, 8)
|
||||
val widthCapacity = if (iconSize > 0) {
|
||||
(root.width - horizontalPadding * 2).coerceAtLeast(0) / iconSize
|
||||
} else 0
|
||||
val rows = CardsGridLayout.arrange(remaining.size, settings, widthCapacity)
|
||||
val location = relativeLocation(container, root) ?: return failOpen(state)
|
||||
if (root.width <= 0 || rows.visibleSlots == 0) return failOpen(state)
|
||||
val iconCount = rows.visibleSlots - if (rows.showsOverflow) 1 else 0
|
||||
val icons = remaining.take(iconCount).map { notificationIcon(root, it, iconSize) }
|
||||
if (icons.any { it == null }) {
|
||||
icons.filterNotNull().forEach(::recycleIcon)
|
||||
return failOpen(state)
|
||||
}
|
||||
|
||||
clearHost(host)
|
||||
host.visibility = View.VISIBLE
|
||||
val rowGap = dp(root, 8)
|
||||
val nativeSurface = if (state.config.hideShelf) findNativeSurface(container) else container
|
||||
if (nativeSurface === root) return failOpen(state)
|
||||
var sourceIndex = 0
|
||||
var slotIndex = 0
|
||||
rows.rowSizes.forEachIndexed { row, count ->
|
||||
val rowWidth = count * iconSize
|
||||
val left = (location.first + container.width / 2 - rowWidth / 2).let {
|
||||
if (settings.limitToScreenWidth) {
|
||||
it.coerceIn(horizontalPadding, (root.width - rowWidth - horizontalPadding).coerceAtLeast(horizontalPadding))
|
||||
} else it
|
||||
}
|
||||
val top = location.second + row * (iconSize + rowGap)
|
||||
if (state.config.showPills) {
|
||||
host.addView(pill(root, nativeSurface, left, top, rowWidth, iconSize))
|
||||
}
|
||||
repeat(count) { column ->
|
||||
val overflow = rows.showsOverflow && slotIndex == rows.visibleSlots - 1
|
||||
val icon = if (overflow) overflowDot(root, iconSize) else icons[sourceIndex++]!!
|
||||
host.addView(icon, frame(iconSize, iconSize, left + column * iconSize, top))
|
||||
slotIndex++
|
||||
}
|
||||
}
|
||||
hideNative(state, nativeSurface)
|
||||
val diagnosticSignature = listOf(remaining.size, state.visibleCardKeys.size, rows.visibleSlots,
|
||||
settings.maxRows, settings.maxIconsPerRow, iconSize).hashCode()
|
||||
if (diagnosticSignature != state.lastDiagnosticSignature) {
|
||||
state.lastDiagnosticSignature = diagnosticSignature
|
||||
Log.i(TAG, "Rendered OneUI Cards grid: cards=${state.visibleCardKeys.size} " +
|
||||
"remaining=${remaining.size} slots=${rows.visibleSlots} overflow=${rows.showsOverflow}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachHost(root: ViewGroup, container: ViewGroup, state: State): FrameLayout? {
|
||||
state.host?.takeIf { it.parent === root }?.let { return it }
|
||||
state.layoutListener?.let { listener -> state.root?.removeOnLayoutChangeListener(listener) }
|
||||
state.host?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
return runCatching {
|
||||
FrameLayout(root.context).apply {
|
||||
tag = HOST_TAG
|
||||
clipChildren = false
|
||||
clipToPadding = false
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
root.addView(this, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
))
|
||||
state.host = this
|
||||
state.root = root
|
||||
val listener = View.OnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) {
|
||||
refresh(container, state.active, state.visibleCardKeys, state.config)
|
||||
}
|
||||
}
|
||||
state.layoutListener = listener
|
||||
root.addOnLayoutChangeListener(listener)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun notificationIcon(root: View, source: Source, size: Int): ImageView? {
|
||||
val drawable = findDrawable(source.view)
|
||||
?: runCatching {
|
||||
val context = source.packageName?.let { root.context.createPackageContext(it, 0) }
|
||||
?: root.context
|
||||
source.smallIcon?.loadDrawable(context)
|
||||
}.getOrNull()
|
||||
?: return null
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val bounds = Rect(drawable.bounds)
|
||||
try {
|
||||
drawable.bounds = aspectFit(drawable, bounds, size)
|
||||
drawable.draw(Canvas(bitmap))
|
||||
} finally {
|
||||
drawable.bounds = bounds
|
||||
}
|
||||
return ImageView(root.context).apply {
|
||||
setImageBitmap(bitmap)
|
||||
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
}
|
||||
|
||||
private fun overflowDot(root: View, size: Int) = View(root.context).apply {
|
||||
background = object : GradientDrawable() {
|
||||
override fun draw(canvas: Canvas) {
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE }
|
||||
val radius = size * 0.125f
|
||||
canvas.drawCircle(size / 2f, size / 2f, radius, paint)
|
||||
}
|
||||
}
|
||||
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
}
|
||||
|
||||
private fun pill(root: View, native: View, left: Int, top: Int, width: Int, height: Int): View {
|
||||
val horizontal = dp(root, 8)
|
||||
val vertical = dp(root, 3)
|
||||
return View(root.context).apply {
|
||||
background = native.background?.constantState?.newDrawable(root.resources)?.mutate()
|
||||
?: GradientDrawable().apply {
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
cornerRadius = (height + vertical * 2) / 2f
|
||||
setColor(DEFAULT_PILL_COLOR)
|
||||
}
|
||||
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
layoutParams = frame(
|
||||
width + horizontal * 2,
|
||||
height + vertical * 2,
|
||||
left - horizontal,
|
||||
top - vertical,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun iconSize(root: View, source: View, settings: CardsGridSettings): Int {
|
||||
val id = root.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
val statusBarHeight = if (id != 0) root.resources.getDimensionPixelSize(id) else 0
|
||||
val fallback = minOf(source.width, source.height).coerceAtLeast(1)
|
||||
return if (statusBarHeight > 0) {
|
||||
(statusBarHeight * settings.iconHeightPercent / 100f).toInt().coerceAtLeast(1)
|
||||
} else fallback
|
||||
}
|
||||
|
||||
private fun relativeLocation(view: View, root: View): Pair<Int, Int>? = runCatching {
|
||||
val viewLocation = IntArray(2)
|
||||
val rootLocation = IntArray(2)
|
||||
view.getLocationOnScreen(viewLocation)
|
||||
root.getLocationOnScreen(rootLocation)
|
||||
viewLocation[0] - rootLocation[0] to viewLocation[1] - rootLocation[1]
|
||||
}.getOrNull()
|
||||
|
||||
private fun findNativeSurface(container: ViewGroup): View {
|
||||
var current = container.parent as? View
|
||||
while (current != null && current !== container.rootView) {
|
||||
if (current.javaClass.name.contains("NotificationShelf")) return current
|
||||
current = current.parent as? View
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
private fun clearHost(host: ViewGroup) {
|
||||
repeat(host.childCount) { index -> recycleIcon(host.getChildAt(index)) }
|
||||
host.removeAllViews()
|
||||
}
|
||||
|
||||
private fun recycleIcon(view: View) {
|
||||
((view as? ImageView)?.drawable as? BitmapDrawable)?.bitmap
|
||||
?.takeUnless(Bitmap::isRecycled)?.recycle()
|
||||
}
|
||||
|
||||
private fun failOpen(state: State) {
|
||||
restoreNative(state)
|
||||
state.host?.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun hideNative(state: State, native: View) {
|
||||
if (state.nativeSurface !== native) {
|
||||
restoreNative(state)
|
||||
state.nativeSurface = native
|
||||
state.nativeAlpha = native.alpha
|
||||
}
|
||||
native.alpha = 0f
|
||||
}
|
||||
|
||||
private fun restoreNative(state: State) {
|
||||
state.nativeSurface?.alpha = state.nativeAlpha
|
||||
state.nativeSurface = null
|
||||
}
|
||||
|
||||
private fun findDrawable(view: View?): Drawable? = when (view) {
|
||||
is ImageView -> view.drawable
|
||||
is ViewGroup -> (0 until view.childCount).firstNotNullOfOrNull { findDrawable(view.getChildAt(it)) }
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun aspectFit(drawable: Drawable, fallback: Rect, size: Int): Rect {
|
||||
val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: fallback.width().coerceAtLeast(1)
|
||||
val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: fallback.height().coerceAtLeast(1)
|
||||
val scale = minOf(size.toFloat() / width, size.toFloat() / height)
|
||||
val targetWidth = (width * scale).toInt().coerceAtLeast(1)
|
||||
val targetHeight = (height * scale).toInt().coerceAtLeast(1)
|
||||
val left = (size - targetWidth) / 2
|
||||
val top = (size - targetHeight) / 2
|
||||
return Rect(left, top, left + targetWidth, top + targetHeight)
|
||||
}
|
||||
|
||||
private fun frame(width: Int, height: Int, left: Int, top: Int) =
|
||||
FrameLayout.LayoutParams(width, height).apply {
|
||||
leftMargin = left
|
||||
topMargin = top
|
||||
}
|
||||
|
||||
private fun dp(view: View, value: Int) = (value * view.resources.displayMetrics.density).toInt()
|
||||
|
||||
private fun state(container: ViewGroup) = states.getOrPut(container, ::State)
|
||||
|
||||
data class Source(
|
||||
val key: String,
|
||||
val view: View?,
|
||||
val smallIcon: android.graphics.drawable.Icon?,
|
||||
val packageName: String? = null,
|
||||
)
|
||||
|
||||
data class Config(
|
||||
val settings: CardsGridSettings,
|
||||
val showPills: Boolean,
|
||||
val hideShelf: Boolean,
|
||||
)
|
||||
|
||||
private class State {
|
||||
var sources = emptyList<Source>()
|
||||
var visibleCardKeys = emptySet<String>()
|
||||
var host: FrameLayout? = null
|
||||
var root: ViewGroup? = null
|
||||
var layoutListener: View.OnLayoutChangeListener? = null
|
||||
var active = false
|
||||
var nativeSurface: View? = null
|
||||
var nativeAlpha = 1f
|
||||
var lastDiagnosticSignature = 0
|
||||
var config = Config(CardsGridSettings.LOCKSCREEN_DEFAULT, showPills = true, hideShelf = true)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HOST_TAG = "notifications-master:cards-grid"
|
||||
const val DEFAULT_PILL_COLOR = 0xAA202124.toInt()
|
||||
const val TAG = "NotificationsMaster"
|
||||
}
|
||||
}
|
||||
+78
@@ -11,6 +11,7 @@ import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
import java.util.function.Function
|
||||
|
||||
/** Hides blocked OneUI notification rows only on the collapsed cards lockscreen. */
|
||||
internal class OneUiLockscreenCardsBackend(
|
||||
@@ -18,6 +19,7 @@ internal class OneUiLockscreenCardsBackend(
|
||||
) : VisibilitySurfaceBackend {
|
||||
private val stacks = Collections.newSetFromMap(WeakHashMap<ViewGroup, Boolean>())
|
||||
private val hiddenRows = WeakHashMap<View, RowState>()
|
||||
private val gridRenderer = OneUiCardsGridRenderer()
|
||||
private lateinit var entryForRow: (View) -> Any?
|
||||
private lateinit var sbnForEntry: (Any) -> StatusBarNotification?
|
||||
private lateinit var shelfNotification: Field
|
||||
@@ -39,6 +41,7 @@ internal class OneUiLockscreenCardsBackend(
|
||||
val stateController = Class.forName(STATE_CONTROLLER, false, classLoader)
|
||||
prepareNotificationAccess(rowClass)
|
||||
installShelfFilter(classLoader)
|
||||
installGridSourceHook(classLoader)
|
||||
|
||||
stackClass.declaredMethods.filter { it.name == "onViewAdded" }.forEach { method ->
|
||||
method.isAccessible = true
|
||||
@@ -148,6 +151,49 @@ internal class OneUiLockscreenCardsBackend(
|
||||
}
|
||||
}
|
||||
|
||||
private fun installGridSourceHook(classLoader: ClassLoader) {
|
||||
val controller = Class.forName(ICON_AREA_CONTROLLER, false, classLoader)
|
||||
val shelfClass = Class.forName(SHELF_ICONS, false, classLoader)
|
||||
controller.declaredMethods.filter { it.name == "updateIconsForLayout" }.forEach { method ->
|
||||
method.isAccessible = true
|
||||
framework.hook(method).intercept { chain ->
|
||||
val functionIndex = chain.args.indexOfFirst { it is Function<*, *> }
|
||||
val container = chain.args.firstOrNull { shelfClass.isInstance(it) } as? ViewGroup
|
||||
if (functionIndex < 0 || container == null) {
|
||||
return@intercept chain.proceed()
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val original = chain.args[functionIndex] as Function<Any?, Any?>
|
||||
val sources = ArrayList<OneUiCardsGridRenderer.Source>()
|
||||
val wrapped = Function<Any?, Any?> { entry ->
|
||||
val result = original.apply(entry)
|
||||
val sbn = entry?.let { runCatching { sbnForEntry(it) }.getOrNull() }
|
||||
if (result is View && sbn != null && !isBlocked(sbn)) {
|
||||
sources += OneUiCardsGridRenderer.Source(
|
||||
sbn.key,
|
||||
result,
|
||||
sbn.notification.smallIcon,
|
||||
sbn.packageName,
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
val arguments = chain.args.toTypedArray()
|
||||
arguments[functionIndex] = wrapped
|
||||
val result = chain.proceed(arguments)
|
||||
gridRenderer.updateSources(container, sources)
|
||||
val active = isLockscreenCards(container)
|
||||
gridRenderer.refresh(
|
||||
container,
|
||||
active,
|
||||
if (active) visibleCardKeys(container) else emptySet(),
|
||||
gridConfig(),
|
||||
)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessor(method: Method?, field: Field?): (Any) -> Any? {
|
||||
method?.isAccessible = true
|
||||
field?.isAccessible = true
|
||||
@@ -165,6 +211,7 @@ internal class OneUiLockscreenCardsBackend(
|
||||
|
||||
private fun applyTrackedStacks() {
|
||||
synchronized(stacks) { stacks.toList() }.forEach(::applyStack)
|
||||
gridRenderer.refreshAll(::isLockscreenCards, ::visibleCardKeys, gridConfig())
|
||||
}
|
||||
|
||||
private fun applyStack(stack: ViewGroup) {
|
||||
@@ -250,6 +297,29 @@ internal class OneUiLockscreenCardsBackend(
|
||||
}
|
||||
}
|
||||
|
||||
private fun visibleCardKeys(container: ViewGroup): Set<String> {
|
||||
val shelfLocation = IntArray(2)
|
||||
runCatching { container.getLocationOnScreen(shelfLocation) }.getOrElse { return emptySet() }
|
||||
val root = container.rootView
|
||||
return synchronized(stacks) { stacks.toList() }
|
||||
.asSequence()
|
||||
.filter { it.rootView === root }
|
||||
.flatMap { stack -> (0 until stack.childCount).asSequence().map(stack::getChildAt) }
|
||||
.filter { row ->
|
||||
row.javaClass.name == ROW && row.visibility == View.VISIBLE && row.alpha > 0.05f &&
|
||||
!booleanField(row, "mInShelf") && !booleanField(row, "mTransformingInShelf")
|
||||
}
|
||||
.filter { row ->
|
||||
val location = IntArray(2)
|
||||
runCatching { row.getLocationOnScreen(location) }.isSuccess && location[1] < shelfLocation[1]
|
||||
}
|
||||
.mapNotNull { row -> runCatching { entryForRow(row)?.let(sbnForEntry)?.key }.getOrNull() }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private fun booleanField(target: Any, name: String) =
|
||||
runCatching { findField(target.javaClass, name)?.getBoolean(target) ?: false }.getOrDefault(false)
|
||||
|
||||
private fun requireField(type: Class<*>, name: String) =
|
||||
findField(type, name) ?: error("OneUI $name field is unavailable")
|
||||
|
||||
@@ -267,6 +337,12 @@ internal class OneUiLockscreenCardsBackend(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun gridConfig() = OneUiCardsGridRenderer.Config(
|
||||
ProcessVisibilityPolicyCache.lockscreenCards,
|
||||
showPills = true,
|
||||
hideShelf = true,
|
||||
)
|
||||
|
||||
private data class RowState(val visibility: Int, val alpha: Float)
|
||||
|
||||
private companion object {
|
||||
@@ -282,6 +358,8 @@ internal class OneUiLockscreenCardsBackend(
|
||||
const val SHELF_ICONS = "com.android.systemui.statusbar.phone.SecShelfNotificationIconContainer"
|
||||
const val STATUS_BAR_ICON_VIEW = "com.android.systemui.statusbar.StatusBarIconView"
|
||||
const val ICON_STATE = "com.android.systemui.statusbar.phone.NotificationIconContainer\$IconState"
|
||||
const val ICON_AREA_CONTROLLER =
|
||||
"com.android.systemui.statusbar.phone.LegacyNotificationIconAreaControllerImpl"
|
||||
const val ONE_UI_LOCKSCREEN_COORDINATOR =
|
||||
"com.android.systemui.statusbar.notification.collection.coordinator.LockScreenNotiIconCoordinator"
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Max rows" />
|
||||
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
||||
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Max icons per row" />
|
||||
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
||||
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/even_distribution" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Even distribution" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/limit_width" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Limit to screen width" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center_vertical" android:orientation="horizontal">
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Max rows" />
|
||||
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
||||
<Spinner android:id="@+id/max_rows" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Max icons per row" />
|
||||
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" />
|
||||
<Spinner android:id="@+id/max_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" android:popupBackground="@drawable/dropdown_popup_background" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/even_distribution" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Even distribution" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/limit_width" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Limit to screen width" />
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:gravity="center_vertical" android:orientation="horizontal">
|
||||
|
||||
Reference in New Issue
Block a user