AppDimens SSP, HSP, WSP
May 16, 2026 · View on GitHub

AppDimens is the most complete responsive typography and dimension library for Android. It provides thousands of pre-calculated @dimen resources ready to use — plus dynamic Compose extensions, code-level APIs, conditional builders, orientation-aware inverters, and accessibility-aware variants — all in a single, zero-configuration dependency.
🛠️ Installation
dependencies {
implementation("io.github.bodenberg:appdimens-ssps:3.1.5")
}
Requirements: Min SDK 24 · Compile SDK 36 · Kotlin & Java · XML & Jetpack Compose
💻 Usage Examples
1. Jetpack Compose
Basic — Auto-Scaling Extensions:
import com.appdimens.ssps.compose.ssp
import com.appdimens.ssps.compose.hsp
import com.appdimens.ssps.compose.wsp
import com.appdimens.ssps.compose.sem
Text(
text = "Smallest Width Scaling",
fontSize = 16.ssp, // Scales relative to the device's smallest width
lineHeight = 20.ssp
)
Text(
text = "Width Scaling",
fontSize = 18.wsp // Scales relative to the device's width
)
Text(
text = "Height Scaling",
fontSize = 20.hsp // Scales relative to the device's height
)
Text(
text = "Fixed Scale",
fontSize = 14.sem // Scales by Smallest Width but IGNORES system font scale
)
Inverter Shortcuts — Orientation-Aware Scaling:
import com.appdimens.ssps.compose.sspPh
import com.appdimens.ssps.compose.sspLw
import com.appdimens.ssps.compose.hspLw
import com.appdimens.ssps.compose.wspLh
// .sspPh → uses Smallest Width by default; in Portrait → switches to Height
val adaptiveVert = 32.sspPh
// .sspLw → uses Smallest Width by default; in Landscape → switches to Width
val adaptiveHorz = 32.sspLw
// .hspLw → uses Height by default; in Landscape → switches to Width
val heightToWidth = 50.hspLw
// .wspLh → uses Width by default; in Landscape → switches to Height
val widthToHeight = 50.wspLh
Aspect-ratio aware (sspa, hspa, wspa, sema, hema, wema): Applies the same default aspect-ratio multiplier as appdimens-dynamic on top of the XML-resolved @dimen value — effectively one extra multiply (finalPx = getDimension(sp px) × arAdjustment). Examples: 16.sspa, 32.hspa, 16.sema. The *ia names (sspia, hemia, …) exist for API parity with dynamic (multi-window “ignore scaling” paths there); with SSPS XML they match the corresponding *a APIs. Adjustment factors invalidate when (smallestScreenWidthDp, screenWidthDp, screenHeightDp, densityDpi) changes. Optional prefetch: DimenSsp.warmupSspsFactors(context). Numeric parity with appdimens-dynamic holds when Android selects the same resource bucket for _1ssp / _1wsp / _1hsp used in the maths.
Sample app: the included app module demonstrates aspect ratio in Compose: open ExampleActivity (package com.example.app.compose) — the Aspect Ratio (with vs without) card compares ssp / hsp / wsp / sem next to sspa / hspa / wspa / sema at the same nominal sizes. The Kotlin and Java sample activities call DimenSsp.warmupSspsFactors and log AR compare lines (pixel values) for ssp vs sspa and hsp vs hspa. Instrumented coverage lives in AppDimensSspsAspectRatioInstrumentedTest in the library.
Facilitators — Quick Conditional Overrides:
import com.appdimens.ssps.common.Orientation
import com.appdimens.ssps.compose.sspRotate
import com.appdimens.ssps.compose.sspMode
import com.appdimens.ssps.compose.sspQualifier
import com.appdimens.ssps.compose.sspScreen
// Rotate Facilitators:
// 1. Int variant (Scales result by default)
val size1 = 16.sspRotate(24) // 16.ssp default, 24.ssp in Landscape
// 2. TextUnit receiver + Int rotation (Plain receiver: keeps raw TextUnit if not in target orientation;
// rotation branch still resolves the Int via resources)
val size2 = 16.ssp.sspRotatePlain(24) // 16.ssp default, 24.ssp in Landscape, raw 16.sp otherwise
// 3. TextUnit variant (Follows Int logic)
val size3 = 16.sp.sspRotate(24) // 16.ssp default, 24.ssp in Landscape
// 4. Two pre-resolved TextUnits — no resource lookup; only orientation branch (same for .hsp/.wsp)
val size4 = 30.ssp.sspRotatePlain(20.ssp)
val size5 = 30.ssp.sspRotatePlain(20.ssp, Orientation.LANDSCAPE) // orientation optional; default is LANDSCAPE
// 5. Nested extensions (inner expression runs first)
val size6 = 16.ssp.sspRotatePlain(20.ssp.sspRotatePlain(14.ssp))
// Other Facilitators:
val modeVal = 16.sspMode(40, UiModeType.TELEVISION)
val qualVal = 16.sspQualifier(24, DpQualifier.SMALL_WIDTH, 600)
val scrVal = 16.sspScreen(32, UiModeType.TELEVISION, DpQualifier.SMALL_WIDTH, 600)
Nesting extensions vs. scaledSp().screen(...)
- You can chain facilitator extensions (e.g. nested
sspRotatePlain). Evaluation order is inside-out, i.e. the order of nesting in code. - For nesting, prefer
sspRotatePlain/hspRotatePlain/wspRotatePlainwith twoTextUnitarguments so neither operand of that call is passed through another resource resolution step. Alternatively, use theTextUnit+TextUnitoverload so both sides are already the final scaled values you want. scaledSp().screen(...)builds a list of rules sorted by priority (and tie-breakers on dp thresholds). The first matching rule wins — this is not the same as nested extension order.- Plain
TextUnit+Int*RotatePlainstill resolves the rotationIntvia the library; only the receiver stays untouched when the orientation does not match.
DimenSspScaled Builder — Complex Multi-Condition Chains:
import com.appdimens.ssps.common.Orientation
import com.appdimens.ssps.compose.scaledSp
val dynamicText = 16.scaledSp()
// Priority 1: TV + sw ≥ 600 → 40.ssp
.screen(UiModeType.TELEVISION, DpQualifier.SMALL_WIDTH, 600, 40.ssp)
// Priority 2: Any TV → 32.ssp
.screen(UiModeType.TELEVISION, customValue = 32.ssp)
// Priority 3: Landscape → 20.wsp
.screen(orientation = Orientation.LANDSCAPE, customValue = 20.wsp)
.ssp // Resolve with Smallest Width adaptation as fallback
2. XML Layouts
Use dimension resources directly — all values from 1 to 600 are pre-generated:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- SSP: Scales based on smallest width -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/_16ssp"
android:text="Smallest Width Scaled" />
<!-- WSP: Scales based on screen width -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/_16wsp"
android:text="Width Scaled" />
<!-- HSP: Scales based on screen height -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/_16hsp"
android:text="Height Scaled" />
</LinearLayout>
3. Kotlin (Code Level)
// Core — Pixel values (converted from Sp)
val fontSizePx = DimenSsp.ssp(context, 16) // Smallest Width
val heightPx = DimenSsp.hsp(context, 32) // Height
val widthPx = DimenSsp.wsp(context, 100) // Width
// Accessibility — Ignore font scaling
val fixedSpPx = DimenSsp.sem(context, 16) // Without font scaling
// Aspect ratio — same XML bucket, extra geometry multiplier (+ optional warmup)
DimenSsp.warmupSspsFactors(context)
val sspPx = DimenSsp.ssp(context, 32)
val sspaPx = DimenSsp.sspa(context, 32)
// Kotlin Extensions
import com.appdimens.ssps.code.ssp
import com.appdimens.ssps.code.hsp
import com.appdimens.ssps.code.scaledSp
val size = 16.ssp(context)
val adaptiveFont = 16.hsp(context)
val withAr = 32.sspa(context)
// DimenSspScaled builder
val builderSp = 16.scaledSp()
.screen(UiModeType.TELEVISION, 40)
.ssp(context)
// Resource IDs
val resId = DimenSsp.sspRes(context, 16)
// Inverter shortcuts
val adaptive = DimenSsp.hspLw(context, 20) // Height → Width in Landscape
// Facilitators
val rotated = DimenSsp.sspRotate(context, 16, Orientation.LANDSCAPE)
val modeVal = DimenSsp.sspMode(context, 16, 32, UiModeType.TELEVISION)
4. Java (Code Level)
// Core
float fontSizePx = DimenSsp.ssp(context, 16);
int resId = DimenSsp.sspRes(context, 16);
// Accessibility
float fixedSpPx = DimenSsp.sem(context, 16);
// Aspect ratio (optional warmup + compare with base axis)
DimenSsp.warmupSspsFactors(context);
float sspPx = DimenSsp.ssp(context, 32);
float sspaPx = DimenSsp.sspa(context, 32);
// Inverter shortcuts
float adaptive = DimenSsp.hspLw(context, 20);
// DimenSspScaled builder (uses @JvmStatic + @JvmOverloads)
DimenSspScaled scaled = DimenSsp.scaled(16)
.screen(UiModeType.TELEVISION, 32)
.screen(DpQualifier.SMALL_WIDTH, 600, 24);
float result = scaled.ssp(context);
✨ What's New in Version 3.x
| Feature | Description |
|---|---|
| Triple Axis Scaling | Full support for SSP (Smallest Width), HSP (Height), and WSP (Width) |
Aspect ratio (*a variants) | sspa, hspa, wspa, sema, hema, wema (+ inverters): geometry multiplier aligned with appdimens-dynamic; optional DimenSsp.warmupSspsFactors. Demonstrated side-by-side in the sample app Compose demo. |
| Accessibility Control | SEM, HEM, WEM variants to ignore system font scale when necessary |
| Code-Level API | Full DimenSsp object for Java & Kotlin — resolve text sizes outside of XML and Compose |
| Inverter Shortcuts | .sspPh, .sspLw, .hspPw, .wspLh, etc. — orientation-aware switching |
| Facilitators | sspRotate, sspMode, sspQualifier, sspScreen — quick conditional overrides |
| Advanced Builders | DimenSspScaled for complex chaining with UiModeType and DpQualifier |
| Foldable Detection | FoldingFeature integration — detects Fold/Flip open/half-open states |
| UiModeType | NORMAL, TELEVISION, CAR, WATCH, FOLD_OPEN, FOLD_HALF, FLIP_OPEN, FLIP_HALF |
🧮 Why Pre-Calculated Scales?
Most responsive Android solutions use runtime calculations to convert dimensions — multiplying density, screen metrics, or ratios on every frame or measure pass. AppDimens takes a fundamentally different approach:
The Problem with Runtime Calculations
// ❌ Runtime calculation approach (common in other libraries)
fun scaledSp(value: Int): Float {
val screenWidth = resources.displayMetrics.widthPixels
val baseWidth = 360f // arbitrary "design" base
return value * (screenWidth / baseWidth) // calculated EVERY call
}
This has several issues:
- CPU Cost — calculated on every call, wasted cycles
- Linear Scaling — simple ratios produce values that are too large on tablets or too small on watches
- No Qualifier Awareness — ignores Android's built-in resource qualifier system (
values-sw600dp, etc.)
The AppDimens Solution: Pre-Calculated + Qualifier-Aware
AppDimens provides thousands of @dimen resources generated with mathematically refined, non-linear scaling curves tuned for each qualifier bucket:
res/
├── values/ → Base values (phones ~320-360dp)
├── values-sw360dp/ → Standard phones
├── values-sw600dp/ → 7" tablets
├── values-sw720dp/ → 10" tablets
├── values-h600dp/ → Height-based qualifiers
├── values-w600dp/ → Width-based qualifiers
└── ... → 350+ qualifier directories
Each value is pre-calculated to produce dimensions tuned specifically for that screen category.
⚡ Performance
XML: Zero Cost
All @dimen/_16ssp resources are resolved statically at build time. No runtime overhead.
Compose: Near-Zero Cost
The .ssp, .hsp, .wsp extensions use:
LocalConfiguration.current— cached by ComposeLocalContext.current.resources.getIdentifier()— native lookupdimensionResource()— standard Compose resolution
No extra state or unnecessary recompositions.
📖 How It Works
Three Scaling Axes
| Qualifier | Extension | Resource | Based On |
|---|---|---|---|
| SSP | .ssp | @dimen/_16ssp | smallestScreenWidthDp — independent of orientation |
| HSP | .hsp | @dimen/_16hsp | screenHeightDp — current screen height |
| WSP | .wsp | @dimen/_16wsp | screenWidthDp — current screen width |
Aspect ratio variants reuse the same @dimen/_Nssp / _Nhsp / _Nwsp resources but apply an extra per-axis multiplier derived from _1ssp / _1wsp / _1hsp and the current (smallestScreenWidthDp, screenWidthDp, screenHeightDp, densityDpi) (Compose: .sspa, .hspa, .wspa; SEM-style: sema, hema, wema; inverter forms such as sspPh → sspPha). Near the reference ~1.78 display ratio, base and *a sizes can coincide; diverging ratios show a clearer delta.
Resource Naming Convention
_{value}{qualifier}
Examples:
_16ssp → 16sp scaled by Smallest Width
_20wsp → 20sp scaled by Width
_20hsp → 20sp scaled by Height
Range: 1 to 600 for all qualifiers.
🏆 Why AppDimens is More Complete
| Feature | AppDimens SSPS | intuit-ssp |
|---|---|---|
| SSP (Smallest Width) | ✅ | ✅ |
| HSP (Height) | ✅ | ❌ |
| WSP (Width) | ✅ | ❌ |
| SEM, HEM, WEM (Ignore font scale) | ✅ | ❌ |
| Compose extensions | ✅ .ssp, .hsp, .wsp + aspect .sspa, .hspa, .wspa, sema, … | ❌ |
Aspect ratio (*a) + warmupSspsFactors | ✅ | ❌ |
| Code-level API | ✅ DimenSsp object | ❌ |
| Conditional builder | ✅ DimenSspScaled | ❌ |
| Folding support | ✅ Fold/Flip states | ❌ |
| Range | 1 to 600 | 1 to 600 |
🚀 Advantages
- Zero Configuration — Works out of the box. No initialization.
- Typography Precision — Triple axis scaling (SSP+HSP+WSP) for perfect layouts.
- Hybrid Integration — Works identically in XML, Compose, and Kotlin/Java code.
- Device-Aware — Built-in detection for TV, Car, Watch, and Foldables.
- Accessibility Friendly — Choose between standard
.sspor restricted.sem. - Zero Performance Impact — Values resolved via Android's native resource system.

Important
Recommended Distribution: It is highly recommended to generate and distribute your application as an AAB (Android App Bundle). This allows the Android system to deliver only the resource qualifiers (like values-sw600dp) that match the specific device downloading the app, significantly reducing the final APK size.
Created with the best traditions of responsive and accessible typography for the Android ecosystem.