AppDimens SSP, HSP, WSP

August 25, 2026 · View on GitHub

AppDimens Banner

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.7")
}

Requirements: Min SDK 24 · Compile SDK 36 · Kotlin & Java · XML & Jetpack Compose


🔀 Which AppDimens Library Should I Use?

AppDimens is a family of libraries — pick the one that matches your project type:

Your projectRecommended library
Jetpack Composeappdimens-dynamic — dynamic, aspect-ratio and orientation-aware Compose-first scaling
Kotlin Multiplatform (KMP)appdimens-kmp — multiplatform SSP/HSP/WSP scaling
XML, Kotlin/Java code, or hybrid (this library)AppDimens SSPS — pre-calculated @dimen resources + Compose extensions + code-level APIs

💻 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 an aspect-ratio adjustment on top of the XML-resolved @dimen value. Examples: 16.sspa, 32.hspa, 16.sema (Compose) and DimenSsp.sspa(ctx, 16) (code). The *ia aliases (sspia, hemia, …) match the corresponding *a APIs. Factors update when (smallestScreenWidthDp, screenWidthDp, screenHeightDp, densityDpi) change. Optional prefetch: DimenSsp.warmupSspsFactors(context).

Sample app: the app module compares ssp / hsp / wsp / sem with sspa / hspa / wspa / sema in Compose (com.example.app.compose.ExampleActivity). Kotlin and Java samples call DimenSsp.warmupSspsFactors. Instrumented coverage: AppDimensSspsAspectRatioInstrumentedTest.

Facilitators — Quick Conditional Overrides:

import com.appdimens.ssps.common.Orientation
import com.appdimens.ssps.compose.AppDimensProvider
import com.appdimens.ssps.compose.sspRotate
import com.appdimens.ssps.compose.sspMode
import com.appdimens.ssps.compose.sspQualifier
import com.appdimens.ssps.compose.sspScreen

// Optional root wrapper — caches UiMode (incl. foldables) for *Mode / *Screen facilitators
setContent { AppDimensProvider { /* app UI */ } }

// 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 / wspRotatePlain with two TextUnit arguments so neither operand of that call is passed through another resource resolution step. Alternatively, use the TextUnit + TextUnit overload 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 *RotatePlain still resolves the rotation Int via 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);

Layout example



✨ What's New in Version 3.x

FeatureDescription
Triple Axis ScalingFull support for SSP (Smallest Width), HSP (Height), and WSP (Width)
Aspect ratio (*a variants)sspa, hspa, wspa, sema, hema, wema (+ inverters): aspect-ratio adjustment on top of XML-resolved sizes; optional DimenSsp.warmupSspsFactors.
Accessibility ControlSEM, HEM, WEM variants to ignore system font scale when necessary
Code-Level APIFull DimenSsp object for Java & Kotlin — resolve text sizes outside of XML and Compose
Inverter Shortcuts.sspPh, .sspLw, .hspPw, .wspLh, etc. — orientation-aware switching
FacilitatorssspRotate, sspMode, sspQualifier, sspScreen — quick conditional overrides
Advanced BuildersDimenSspScaled for complex chaining with UiModeType and DpQualifier
Foldable DetectionFoldingFeature integration — detects Fold/Flip open/half-open states
UiModeTypeNORMAL, 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 Compose
  • LocalContext.current.resources.getIdentifier() — native lookup
  • dimensionResource() — standard Compose resolution

No extra state or unnecessary recompositions.


📖 How It Works

Three Scaling Axes

QualifierExtensionResourceBased On
SSP.ssp@dimen/_16sspsmallestScreenWidthDp — independent of orientation
HSP.hsp@dimen/_16hspscreenHeightDp — current screen height
WSP.wsp@dimen/_16wspscreenWidthDp — 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 sspPhsspPha). 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

FeatureAppDimens SSPSintuit-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 APIDimenSsp object
Conditional builderDimenSspScaled
Folding support✅ Fold/Flip states
Range1 to 6001 to 600

🚀 Advantages

  1. Zero Configuration — Works out of the box. No initialization.
  2. Typography Precision — Triple axis scaling (SSP+HSP+WSP) for perfect layouts.
  3. Hybrid Integration — Works identically in XML, Compose, and Kotlin/Java code.
  4. Device-Aware — Built-in detection for TV, Car, Watch, and Foldables.
  5. Accessibility Friendly — Choose between standard .ssp or restricted .sem.
  6. Zero Performance Impact — Values resolved via Android's native resource system.

Extra demonstration

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.