Skip to content

Latest commit

 

History

History
913 lines (709 loc) · 27.2 KB

File metadata and controls

913 lines (709 loc) · 27.2 KB

📐 AppDimens: Simplified Mathematical Theory

Hub documentation. Conceptual reference for scaling theory in the AppDimens family. Versions, semver, and install commands belong only in platform submodule READMEs linked from README.md—not here.

Validated Android mapping: BALANCED→auto, FIXED + DEFAULT→scaled, DYNAMIC + catalog PERCENTAGE→percent (see NAMING_AND_MIGRATION_1X_2X_TO_3X.md, IMPLEMENTATION_ALIGNMENT_APPDIMENS_DYNAMIC.md, MATHEMATICS-AND-CALCULUS.md).


Quick & Easy Guide - Understand in 15 Minutes
Author: Jean Bodenberg

API note: Jetpack Compose snippets refer to the current appdimens-dynamic submodule (sdp, hdp, wdp, ssp, and asdp / ahdp / awdp for hybrid auto axes). Other platforms may expose balanced() style builders—see Platform API map.

Scope: Thirteen scaling strategies, BALANCED primary, DEFAULT secondary—confirm Compose vs builder APIs via Platform API map.

flowchart LR
  phone["Phones"] --> Balanced["Strategy BALANCED"]
  tablet["Tablets_TV"] --> Balanced
  Balanced --> grow["Controlled growth curve"]
Loading

📚 Related Documentation:


📋 Table of Contents

  1. The Problem We're Solving
  2. Current catalog highlights
  3. PRIMARY: BALANCED Strategy (Recommended)
  4. SECONDARY: DEFAULT Strategy (Phone-Focused)
  5. Other Useful Strategies
  6. Quick Decision Guide
  7. Platform Examples
  8. Performance & Optimization

1. The Problem We're Solving

The Traditional Sizing Problem

Imagine you design a button as 48dp (density-independent pixels) on a phone:

📱 Phone (360dp width):  48dp button = 13.3% of screen  ✅ Good!
📱 Tablet (720dp width): 48dp button = 6.7% of screen   ❌ Too small!
📺 TV (1080dp width):    48dp button = 4.4% of screen   ❌ Tiny!

Traditional approach: Button stays 48dp everywhere → looks tiny on large screens

The Linear Scaling Problem

Simple solution: scale proportionally (like SDP/SSP):

📱 Phone (360dp):   48dp × (360/360) = 48dp   ✅ Good
📱 Tablet (720dp):  48dp × (720/360) = 96dp   ❌ Too big!
📺 TV (1080dp):     48dp × (1080/360) = 144dp ❌ Huge!

Linear scaling: Grows too aggressively → elements become oversized

The AppDimens Solution

AppDimens offers 13 smart strategies that scale intelligently based on psychophysics research:

📱 Phone (360dp):   BALANCED → 57.6dp  ✅ Perfect
📱 Tablet (720dp):  BALANCED → 69.7dp  ✅ Perfect! (not 96dp)
📺 TV (1080dp):     BALANCED → 100.9dp ✅ Perfect! (not 144dp)

Result: Elements grow enough to be visible, but not excessively large!


2. Current catalog highlights

Evolution from earlier revisions

AppDimens earlier revisions:

  • ✅ 2 strategies: Fixed, Dynamic
  • ✅ Logarithmic scaling
  • ✅ Aspect ratio compensation

Unified catalog highlights

  • 13 strategies (from 2)
  • BALANCED - New primary recommendation (hybrid linear-logarithmic)
  • Perceptual models (Weber-Fechner, Stevens' Power Law)
  • Smart Inference (automatic strategy selection)
  • 5x performance improvement
  • Full backward compatibility (old code still works)

Naming Changes

For clarity, current catalog renamed the original strategies:

Legacy name Canonical name Typical use
Fixed DEFAULT Phone-focused apps, icons
Dynamic PERCENTAGE Large containers, proportional

New primary recommendation: BALANCED ⭐ (best for multi-device apps)


3. PRIMARY: BALANCED Strategy (Recommended)

Primary recommendation: BALANCED suits most multi-form-factor products—verify builders in submodule docs.

How It Works

BALANCED combines two behaviors:

  1. Linear on phones (< 480dp): Familiar, proportional scaling
  2. Logarithmic on tablets/TVs (≥ 480dp): Controls oversizing
         Linear Region        │    Logarithmic Region
    (phones: 240-480dp)       │   (tablets/TVs: 480+dp)
                              │
  ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱  │  ╱╱╱──────────────
  Proportional growth         │  Controlled growth
                             480dp transition

The Math (Simple Version)

For phones (< 480dp):

Result = Base × (ScreenWidth / 300)

Example: 48dp on 360dp phone
Result = 48 × (360/300) = 57.6dp

For tablets/TVs (≥ 480dp):

Result = Base × [1.6 + 0.40 × ln(1 + (Width-480)/300)]

Example: 48dp on 720dp tablet
Result = 48 × [1.6 + 0.40 × ln(1.8)]
       = 48 × [1.6 + 0.235]
       = 48 × 1.835
       ≈ 88dp (actual: ~70dp after optimization)

Key insight: Growth slows down on large screens (logarithmic)

Visual Comparison

Screen LINEAR (bad) BALANCED ⭐ Difference
300dp 48dp 48dp Reference
360dp 57.6dp 57.6dp Same
480dp 76.8dp 76.8dp Transition
600dp 96.0dp 85.0dp -11dp
720dp 115.2dp 69.7dp -45dp
1080dp 172.8dp 100.9dp -72dp

Benefits:

  • Phones: Familiar linear behavior (same as SDP/SSP)
  • Tablets: 11-45dp reduction (prevents oversizing)
  • TVs: 72dp reduction (buttons stay touchable size)

When to Use BALANCED

✅ Perfect For:

  • Multi-device apps (phones + tablets + TVs)
  • Social media apps
  • Productivity apps
  • News and content apps
  • E-commerce apps
  • Most general-purpose applications

❌ Consider Alternatives:

  • Phone-only apps → Use DEFAULT instead
  • Large containers → Use PERCENTAGE
  • Game UI → Use FIT or FILL

Platform Examples

Android (Kotlin)

@Composable
fun MyButton() {
    Button(
        onClick = { },
        modifier = Modifier
            .height(48.asdp)  // auto — hybrid BALANCED curve
            .fillMaxWidth()
    ) {
        Text(
            text = "Click Me",
            fontSize = 16.assp
        )
    }
}

iOS (Swift)

struct MyButton: View {
    var body: some View {
        Button("Click Me") { }
            .frame(height: AppDimens.shared.balanced(48).toPoints())
            .font(.system(size: AppDimens.shared.balanced(16).toPoints()))
    }
}

Flutter (Dart)

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    minimumSize: Size(double.infinity, AppDimens.fixed(48).calculate(context)),
  ),
  child: Text(
    'Click Me',
    style: TextStyle(fontSize: AppDimens.fixed(16).calculate(context)),
  ),
)

React Native (TypeScript)

{% raw %}

const {balanced} = useAppDimens();

<TouchableOpacity style={{height: balanced(48)}}>
  <Text style={{fontSize: balanced(16)}}>Click Me</Text>
</TouchableOpacity>

{% endraw %}

Web (TypeScript)

{% raw %}

const {balanced} = useWebDimens();

<button style={{height: balanced(48), fontSize: balanced(16)}}>
  Click Me
</button>

{% endraw %}


4. SECONDARY: DEFAULT Strategy (Phone-Focused)

Secondary Recommendation: Use DEFAULT for phone-focused apps where you want slightly more control than linear scaling.

How It Works

DEFAULT (formerly "Fixed" in earlier revisions) provides:

  • ~97% linear growth component
  • ~3% logarithmic adjustment based on aspect ratio
  • Automatic compensation for elongated screens (20:9, 21:9)

The Math (Simple Version)

Result = Base × [1 + ((Width-300)/1) × (0.00333 + 0.00267 × ln(AR/1.78))]

Where:
- Width: Screen width in dp
- AR: Aspect ratio (e.g., 2.22 for 20:9)
- 0.00333: Base increment (~97% linear component)
- 0.00267: AR sensitivity (~3% logarithmic component)

Simplified: Almost linear, with small AR adjustment

Visual Comparison

Screen LINEAR DEFAULT BALANCED ⭐
300dp 48dp 48dp 48dp
360dp 57.6dp 53.8dp 57.6dp
480dp 76.8dp 64.5dp 76.8dp
720dp 115.2dp 79.2dp 69.7dp
1080dp 172.8dp 94.0dp 100.9dp

Observations:

  • DEFAULT is more conservative than LINEAR
  • DEFAULT provides moderate control on large screens
  • BALANCED provides even more control on tablets/TVs

When to Use DEFAULT

✅ Perfect For:

  • Phone-only applications (320-480dp range)
  • Apps with elongated screens (20:9, 21:9 aspect ratios)
  • Icons and small UI elements
  • Backward compatibility with AppDimens earlier revisions
  • When you want slightly more control than BALANCED on phones

❌ Consider Alternatives:

  • Multi-device apps → Use BALANCED (better tablet/TV behavior)
  • Maximum control needed → Use LOGARITHMIC

Platform Examples

Android (Kotlin)

import com.appdimens.dynamic.compose.*

Icon(
    imageVector = Icons.Default.Favorite,
    modifier = Modifier.size(24.sdp),  // scaled — conceptual DEFAULT / phone-first
    tint = Color.Red
)

iOS (Swift)

Image(systemName: "heart.fill")
    .resizable()
    .frame(
        width: AppDimens.shared.defaultScaling(24).toPoints(),
        height: AppDimens.shared.defaultScaling(24).toPoints()
    )

Flutter (Dart)

Icon(
  Icons.favorite,
  size: AppDimens.fixed(24).calculate(context),
  color: Colors.red,
)

5. Other Useful Strategies

5.1 PERCENTAGE (Proportional)

Formula: Result = Base × (Width / 300)

When to Use:

  • ✅ Very large containers
  • ✅ Proportional images
  • ✅ Full-width grids
  • ❌ Don't use for buttons or text!

Example:

LazyVerticalGrid(
    columns = GridCells.Adaptive(120.wdp)
)

5.2 LOGARITHMIC (Maximum Control)

Formula: Result = Base × (1 + 0.40 × ln(Width/300))

When to Use:

  • ✅ TV applications (1080dp+)
  • ✅ Very large tablets
  • ✅ When oversizing is a major concern
  • ❌ Avoid for phones (may reduce sizes)

Result on 720dp: 67.2dp (vs 115.2dp linear, -42% reduction)

5.3 POWER (Configurable)

Formula: Result = Base × (Width/300)^0.75

When to Use:

  • ✅ General-purpose apps
  • ✅ When you want configurable control
  • ✅ Scientific/research applications

Exponent range: 0.60-0.90 (0.75 is default)

5.4 FLUID (CSS Clamp-Like)

Formula: Interpolates between min and max values

When to Use:

  • ✅ Typography with size bounds
  • ✅ Line heights
  • ✅ Spacing with explicit limits

Example:

Text(
    text = "Article Title",
    fontSize = fluidSp(minValue = 18f, maxValue = 32f)
)

5.5 FIT & FILL (Games)

FIT (Letterbox):

Result = Base × min(Width/300, Height/533)

FILL (Cover):

Result = Base × max(Width/300, Height/533)

When to Use:

  • ✅ Game UI elements
  • ✅ Full-screen content
  • ✅ Background images

5.6 AUTOSIZE 🆕 (Container-Aware)

Concept: Auto-adjusts to fit container (like TextView autoSizeText)

When to Use:

  • ✅ Dynamic text that must fit
  • ✅ Variable-size containers
  • ✅ Auto-sizing typography

5.7 NONE (No Scaling)

Formula: Result = Base (unchanged)

When to Use:

  • ✅ Dividers (always 1dp)
  • ✅ Fixed-size icons
  • ✅ System UI elements

6. Quick Decision Guide

Choose Your Strategy in 30 Seconds

┌─ What type of app? ─────────────────────────────────┐
│                                                      │
├─ Multi-device (phones + tablets + TVs)?             │
│  └─ YES → BALANCED ⭐ (Primary Recommendation)       │
│                                                      │
├─ Phone-only app?                                    │
│  └─ YES → DEFAULT (Secondary Recommendation)        │
│                                                      │
├─ Large containers, images, grids?                   │
│  └─ YES → PERCENTAGE                                │
│                                                      │
├─ TV/large tablet focus?                             │
│  └─ YES → LOGARITHMIC                               │
│                                                      │
├─ Typography with bounds?                            │
│  └─ YES → FLUID                                     │
│                                                      │
├─ Game development?                                  │
│  └─ YES → FIT or FILL                               │
│                                                      │
└─ Not sure? → Use BALANCED ⭐ (works for 95% of apps)│

Strategy Comparison Table

Strategy Phone Tablet TV Use Case
BALANCED ✅✅✅ ✅✅✅ ✅✅✅ Multi-device (PRIMARY)
DEFAULT ✅✅✅ ✅✅ Phone-focused (SECONDARY)
PERCENTAGE ✅✅ ⚠️ Large containers
LOGARITHMIC ⚠️ ✅✅ ✅✅✅ Maximum control
POWER ✅✅ ✅✅ ✅✅ Configurable
FLUID ✅✅ ✅✅ ✅✅ Typography

7. Platform Examples

Complete Example: Social Media Post Card

Android (Jetpack Compose)

Same BALANCED scenario: use auto tokens (asdp / assp). Plain sdp / ssp map to scaled, not the hybrid curve.

@Composable
fun PostCard(post: Post) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.asdp)
    ) {
        Column(modifier = Modifier.padding(16.asdp)) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Image(
                    painter = rememberImagePainter(post.avatar),
                    modifier = Modifier
                        .size(40.asdp)
                        .clip(CircleShape)
                )
                Spacer(modifier = Modifier.width(12.asdp))
                Column {
                    Text(
                        text = post.username,
                        fontSize = 14.assp,
                        fontWeight = FontWeight.Bold
                    )
                    Text(
                        text = post.timestamp,
                        fontSize = 12.assp,
                        color = Color.Gray
                    )
                }
            }

            Spacer(modifier = Modifier.height(12.asdp))

            Text(
                text = post.content,
                fontSize = 14.assp,
                lineHeight = 20.assp
            )

            Spacer(modifier = Modifier.height(12.asdp))

            Row {
                IconButton(
                    onClick = { },
                    modifier = Modifier.size(40.asdp)
                ) {
                    Icon(Icons.Default.Favorite, "Like")
                }
                IconButton(
                    onClick = { },
                    modifier = Modifier.size(40.asdp)
                ) {
                    Icon(Icons.Default.Comment, "Comment")
                }
                IconButton(
                    onClick = { },
                    modifier = Modifier.size(40.asdp)
                ) {
                    Icon(Icons.Default.Share, "Share")
                }
            }
        }
    }
}

iOS (SwiftUI)

struct PostCard: View {
    let post: Post
    
    var body: some View {
        VStack(alignment: .leading, spacing: AppDimens.shared.balanced(12).toPoints()) {
            // Header
            HStack(spacing: AppDimens.shared.balanced(12).toPoints()) {
                AsyncImage(url: post.avatarURL)
                    .frame(
                        width: AppDimens.shared.balanced(40).toPoints(),
                        height: AppDimens.shared.balanced(40).toPoints()
                    )
                    .clipShape(Circle())
                
                VStack(alignment: .leading) {
                    Text(post.username)
                        .font(.system(size: AppDimens.shared.balanced(14).toPoints()))
                        .fontWeight(.bold)
                    Text(post.timestamp)
                        .font(.system(size: AppDimens.shared.balanced(12).toPoints()))
                        .foregroundColor(.gray)
                }
            }
            
            // Content
            Text(post.content)
                .font(.system(size: AppDimens.shared.balanced(14).toPoints()))
                .lineSpacing(AppDimens.shared.balanced(6).toPoints())
            
            // Actions
            HStack(spacing: AppDimens.shared.balanced(8).toPoints()) {
                Button(action: {}) {
                    Image(systemName: "heart")
                        .frame(
                            width: AppDimens.shared.balanced(40).toPoints(),
                            height: AppDimens.shared.balanced(40).toPoints()
                        )
                }
                Button(action: {}) {
                    Image(systemName: "bubble.right")
                        .frame(
                            width: AppDimens.shared.balanced(40).toPoints(),
                            height: AppDimens.shared.balanced(40).toPoints()
                        )
                }
                Button(action: {}) {
                    Image(systemName: "square.and.arrow.up")
                        .frame(
                            width: AppDimens.shared.balanced(40).toPoints(),
                            height: AppDimens.shared.balanced(40).toPoints()
                        )
                }
            }
        }
        .padding(AppDimens.shared.balanced(16).toPoints())
        .background(Color.white)
        .cornerRadius(8)
        .shadow(radius: 2)
    }
}

Flutter (Dart)

class PostCard extends StatelessWidget {
  final Post post;
  
  const PostCard({required this.post});
  
  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.symmetric(
        vertical: AppDimens.fixed(8).calculate(context)
      ),
      child: Padding(
        padding: EdgeInsets.all(AppDimens.fixed(16).calculate(context)),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header
            Row(
              children: [
                CircleAvatar(
                  radius: AppDimens.fixed(20).calculate(context),
                  backgroundImage: NetworkImage(post.avatarUrl),
                ),
                SizedBox(width: AppDimens.fixed(12).calculate(context)),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      post.username,
                      style: TextStyle(
                        fontSize: AppDimens.fixed(14).calculate(context),
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      post.timestamp,
                      style: TextStyle(
                        fontSize: AppDimens.fixed(12).calculate(context),
                        color: Colors.grey,
                      ),
                    ),
                  ],
                ),
              ],
            ),
            
            SizedBox(height: AppDimens.fixed(12).calculate(context)),
            
            // Content
            Text(
              post.content,
              style: TextStyle(
                fontSize: AppDimens.fixed(14).calculate(context),
                height: 1.4,
              ),
            ),
            
            SizedBox(height: AppDimens.fixed(12).calculate(context)),
            
            // Actions
            Row(
              children: [
                IconButton(
                  icon: Icon(Icons.favorite_border),
                  iconSize: AppDimens.fixed(24).calculate(context),
                  onPressed: () {},
                ),
                IconButton(
                  icon: Icon(Icons.comment_outlined),
                  iconSize: AppDimens.fixed(24).calculate(context),
                  onPressed: () {},
                ),
                IconButton(
                  icon: Icon(Icons.share_outlined),
                  iconSize: AppDimens.fixed(24).calculate(context),
                  onPressed: () {},
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

8. Performance & Optimization

Current catalog Performance Improvements

5x Overall Speedup through mathematical optimizations:

Optimization earlier revisions current catalog Improvement
Cache (Views) 0.005µs 0.001µs 5x
Ln() calculation 0.012µs 0.001µs 10-20x*
Memory per entry 280B 56B 5x
Multi-thread 25% 100% 4x

*Ln() improvement applies to 85-95% of cases (lookup table hits)

How Optimizations Work

1. Ln() Lookup Table (10-20x faster)

Problem: ln() is slow (~0.012µs per call)

Solution: Pre-calculated lookup table with 55 common values

Common values cached:
- Aspect ratios: 1.33 (4:3), 1.78 (16:9), 2.0 (18:9), 2.22 (20:9), etc.
- Screen ratios: 0.8, 1.0, 1.2, 1.6, 2.4, 3.6, etc.

Hit rate: 85-95% (most common scenarios)
Lookup time: 0.001µs (vs 0.012µs for Math.log())

2. Unified Lock-Free Cache (5x faster)

Problem: earlier revisions Views cache had thread-safe locks (slow)

Solution: Lock-free design with Int hash keys

Benefits:
- No lock contention (100% parallelism)
- Int keys faster than object keys
- Ring buffer auto-cleanup
- Zero dependency tracking

3. Pre-Calculated Constants

Problem: Repeated calculations (sqrt, divisions)

Solution: Compile-time pre-calculation

// Before (calculated every time)
val diagonal = sqrt(W² + H²) / sqrt(300² + 533²)

// After (pre-calculated)
const val BASE_DIAGONAL = 611.6305f
val diagonal = sqrt(W² + H²) / BASE_DIAGONAL  // 5x faster

Performance Tips

✅ Do:

  • On Android Compose, prefer sdp / wdp / hdp / ssp and asdp / … when you want the hybrid curve (Platform API map)
  • Enable cache where the platform builder exposes it
  • Warm up expensive paths only if your submodule documents it
  • On iOS / Web / RN, keep using the balanced() helpers those packages export

❌ Don't:

  • Disable cache unnecessarily
  • Use excessive custom qualifiers
  • Calculate dimensions in tight loops
  • Mix strategies randomly (be consistent)

9. Migration from older snippets

Backward compatibility

Depends on which artifact you ship. This chapter’s theory still uses the names BALANCED / DEFAULT / PERCENTAGE, but Android appdimens-dynamic publishes per-strategy packages: sdp / wdp (scaled), asdp / assp (auto, hybrid BALANCED), psdp, …

Naming changes (Android Compose)

Legacy meta-doc appdimens-dynamic 3.x (Compose)
.fxdp / “fixed dp” *.sdp / axis-specific scaled tokens + imports from compose.*
.dydp / “dynamic dp” *.wdp / compose.percent helpers (see submodule)
.balanced().dp (unified) asdp / ahdp / awdp (compose.auto) for the hybrid curve, or sdp for plain scaled

Recommended path

import com.appdimens.dynamic.compose.*
import com.appdimens.dynamic.compose.auto.asdp
import com.appdimens.dynamic.compose.auto.assp

Text("Hello", fontSize = 16.ssp)
Button(modifier = Modifier.height(48.hdp).padding(16.asdp))
Text("Hybrid text", fontSize = 16.assp)

For automatic “smart” selection, use the iOS / Web / React Native helpers documented in those submodules; on Android pick the strategy package explicitly.


10. Summary & Recommendations

Quick Recap

🆕 Current catalog Highlights:

  • 13 strategies (vs 2 in earlier revisions)
  • BALANCED primary recommendation ⭐
  • 5x performance improvement
  • Smart Inference system
  • Full backward compatibility

Strategy Selection Summary

For 95% of apps (Android Compose):

// Hybrid curve on axis ≈ conceptual BALANCED
16.asdp / 16.assp

For phone-first layouts (Android Compose):

// Scaled tokens ≈ conceptual DEFAULT / “fixed-ish” growth
16.sdp / 16.ssp

For large containers (Android Compose):

// Width-biased scaling ≈ conceptual PERCENTAGE
300.wdp

For special cases:

  • TV apps → LOGARITHMIC
  • Typography → FLUID
  • Games → FIT/FILL
  • Fixed sizes → NONE

Key Takeaways

  1. BALANCED is now primary (hybrid linear-logarithmic)
  2. DEFAULT is secondary (phone-focused, backward compatible)
  3. 13 strategies cover all use cases
  4. 5x faster with mathematical optimizations
  5. Works everywhere (Android, iOS, Flutter, React Native, Web)

Next Steps

  1. Install AppDimens on your platform
  2. Start with BALANCED for new code
  3. Read examples for your platform
  4. Experiment with different strategies
  5. Migrate gradually from earlier revisions (if applicable)

📚 Further Reading

Detailed Documentation:

Platform-Specific:

Quick Reference:


Document created by: Jean Bodenberg
License: Apache 2.0
Repository: https://github.com/bodenberg/appdimens


⬆ Back to Top

"Simplicity is the ultimate sophistication. Complex mathematics, simple API."
— AppDimens Philosophy