AppDimens SSP, HSP, WSP

May 16, 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.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 / 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): geometry multiplier aligned with appdimens-dynamic; optional DimenSsp.warmupSspsFactors. Demonstrated side-by-side in the sample app Compose demo.
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.