Skip to content

Latest commit

 

History

History
906 lines (706 loc) · 26.7 KB

File metadata and controls

906 lines (706 loc) · 26.7 KB

📐 AppDimens: Simplified Mathematical Theory

Languages: English | Português (BR) | Español

Quick & Easy Guide - Understand in 15 Minutes
Author: Jean Bodenberg
Date: February 2025
Version: 2.0.0

API note: Android / Jetpack Compose examples refer to appdimens-dynamic 3.x (sdp, hdp, wdp, ssp, and asdp / ahdp / awdp for the auto / “BALANCED-like” hybrid on an axis). Names like balanced() may still appear for iOS, Web, React Native, or in conceptual multi-platform wording from older unified docs. See Platform API map.

🆕 Version 2.0: This guide now covers 13 scaling strategies (up from 2), with BALANCED as the primary recommendation for most apps, and DEFAULT as the secondary choice for phone-focused apps.

📚 Related Documentation:


📋 Table of Contents

  1. The Problem We're Solving
  2. Version 2.0: What's New
  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. Version 2.0: What's New

Evolution from v1.x

AppDimens v1.x:

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

AppDimens v2.0: ⭐ Major Update

  • 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, v2.0 renamed the original strategies:

v1.x Name v2.0 Name When to 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)

🆕 v2.0 Primary Recommendation: Use BALANCED for 95% of applications, especially those targeting multiple device form factors.

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.sdp)  // ⭐ BALANCED
            .fillMaxWidth()
    ) {
        Text(
            text = "Click Me",
            fontSize = 16.ssp
        )
    }
}

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 v1.x) 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 v1.x
  • 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)

@Composable
fun PostCard(post: Post) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.sdp)
    ) {
        Column(modifier = Modifier.padding(16.sdp)) {
            // Header with avatar and username
            Row(verticalAlignment = Alignment.CenterVertically) {
                Image(
                    painter = rememberImagePainter(post.avatar),
                    modifier = Modifier
                        .size(40.sdp)
                        .clip(CircleShape)
                )
                Spacer(modifier = Modifier.width(12.sdp))
                Column {
                    Text(
                        text = post.username,
                        fontSize = 14.ssp,
                        fontWeight = FontWeight.Bold
                    )
                    Text(
                        text = post.timestamp,
                        fontSize = 12.ssp,
                        color = Color.Gray
                    )
                }
            }
            
            Spacer(modifier = Modifier.height(12.sdp))
            
            // Post content
            Text(
                text = post.content,
                fontSize = 14.ssp,
                lineHeight = 20.ssp
            )
            
            Spacer(modifier = Modifier.height(12.sdp))
            
            // Action buttons
            Row {
                IconButton(
                    onClick = { },
                    modifier = Modifier.size(40.sdp)
                ) {
                    Icon(Icons.Default.Favorite, "Like")
                }
                IconButton(
                    onClick = { },
                    modifier = Modifier.size(40.sdp)
                ) {
                    Icon(Icons.Default.Comment, "Comment")
                }
                IconButton(
                    onClick = { },
                    modifier = Modifier.size(40.sdp)
                ) {
                    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

Version 2.0 Performance Improvements

5x Overall Speedup through mathematical optimizations:

Optimization v1.x v2.0 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: v1.x 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 3.x) moved to per-strategy packages with tokens such as sdp, wdp, asdp, assp, etc.

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

🆕 Version 2.0 Highlights:

  • 13 strategies (vs 2 in v1.x)
  • 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 2.0 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 v1.x (if applicable)

📚 Further Reading

Detailed Documentation:

Platform-Specific:

Quick Reference:


Document created by: Jean Bodenberg
Last updated: February 2025
Version: 2.0.0
License: Apache 2.0
Repository: https://github.com/bodenberg/appdimens


⬆ Back to Top

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