Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,61 @@ struct MyContentModifier : ContentModifier {
}
```

### Material 3 wavy progress indicators

On Android, `ProgressView` normally uses Material 3 `LoadingIndicator`, `LinearProgressIndicator`, and `CircularProgressIndicator`. To use the expressive wavy indicators ([`LinearWavyProgressIndicator`](https://developer.android.com/reference/kotlin/androidx/compose/material3/LinearWavyProgressIndicator), [`CircularWavyProgressIndicator`](https://developer.android.com/reference/kotlin/androidx/compose/material3/CircularWavyProgressIndicator.composable)), apply the `.material3WavyProgress()` modifier.

```swift
extension View {
public func material3WavyProgress(wavy: Bool = true, amplitude: ((Double?) -> Double)? = nil, wavelength: Double? = nil, waveSpeed: Double? = nil) -> any View
/// Convenience overload: constant amplitude for every progress value (same as `{ _ in amplitude }`).
public func material3WavyProgress(wavy: Bool = true, amplitude fixedAmplitude: Double, wavelength: Double? = nil, waveSpeed: Double? = nil) -> any View
}

public struct Material3WavyProgressConfiguration: Equatable {
public var isEnabled: Bool
public var amplitude: ((Double?) -> Double)?
public var wavelength: Double?
public var waveSpeed: Double?
}
```

In Skip Fuse, use it like this:

```swift
ProgressView(value: 0.4)
#if os(Android)
.composeModifier { Material3WavyProgressModifier() }
#endif

...

#if SKIP
struct Material3WavyProgressModifier: ContentModifier {
func modify(view: any View) -> any View {
view.material3WavyProgress()
}
}
#endif
```

In Skip Lite, use it like this:

```swift
ProgressView(value: 0.4)
#if SKIP
.material3WavyProgress()
#endif
```

Values are stored in the SwiftUI environment and affect descendant `ProgressView` instances, similar to the other `.material3` modifiers above, so you can use `.material3WavyProgress()` at the top level of your app and make all of your progress indicators wavy.

You can configure the amplitude, wavelength, and wavespeed of wavy progress indicators.

* The default amplitude is 1.0. 0.0 represents no amplitude, and 1.0 represents an amplitude that will take the full height of the progress indicator. You can pass a closure to set the amplitude based on the current measured progress from 0.0 to 1.0 (`nil` for indeterminate progress).
* The default wavelength comes from [`WavyProgressIndicatorDefaults`](https://developer.android.com/reference/kotlin/androidx/compose/material3/WavyProgressIndicatorDefaults).
* The default wavespeed (measured in DP per second) is equal to the wavelength, rendering an animation that moves the wave at one wavelength per second.

### Material Effects

Compose applies an automatic "ripple" effect to components on tap. You can customize the color and alpha of this effect with the `material3Ripple` modifier. To disable the effect altogether, return `nil` from your modifier closure.
Expand Down
3 changes: 2 additions & 1 deletion Sources/SkipUI/Skip/skip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ settings:
- 'version("androidx-appcompat", "1.7.1")'
- 'version("androidx-activity", "1.13.0")'
- 'version("androidx-lifecycle-process", "2.10.0")'
- 'version("androidx-material3", "1.5.0-alpha16")'
- 'version("androidx-material3-adaptive", "1.2.0")'
- 'version("androidx-work", "2.11.1")'

Expand All @@ -26,7 +27,7 @@ settings:
- 'library("androidx-compose-ui-tooling", "androidx.compose.ui", "ui-tooling").withoutVersion()'
- 'library("androidx-compose-animation", "androidx.compose.animation", "animation").withoutVersion()'
- 'library("androidx-compose-material", "androidx.compose.material", "material").withoutVersion()'
- 'library("androidx-compose-material3", "androidx.compose.material3", "material3").withoutVersion()'
- 'library("androidx-compose-material3", "androidx.compose.material3", "material3").versionRef("androidx-material3")'
- 'library("androidx-compose-material-icons-extended", "androidx.compose.material", "material-icons-extended").withoutVersion()'
- 'library("androidx-compose-foundation", "androidx.compose.foundation", "foundation").withoutVersion()'
- 'library("androidx-appcompat", "androidx.appcompat", "appcompat").versionRef("androidx-appcompat")'
Expand Down
101 changes: 94 additions & 7 deletions Sources/SkipUI/SkipUI/Components/ProgressView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,59 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LinearWavyProgressIndicator
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.LoadingIndicatorDefaults
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.WavyProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
#endif

#if SKIP
/// Configuration for Material 3 expressive wavy progress indicators on Android.
///
/// For ``amplitude``, pass `nil` to use a default of `1.0`.
/// The closure receives `nil` for indeterminate indicators; for determinate indicators it receives the current fraction in `0...1`.
/// Material’s wavy indicators coerce amplitude into the valid range.
public struct Material3WavyProgressConfiguration: Equatable {
public var isEnabled: Bool
public var amplitude: ((Double?) -> Double)?
public var wavelength: Double?
public var waveSpeed: Double?

public init(isEnabled: Bool, amplitude: ((Double?) -> Double)? = nil, wavelength: Double? = nil, waveSpeed: Double? = nil) {
self.isEnabled = isEnabled
self.amplitude = amplitude
self.wavelength = wavelength
self.waveSpeed = waveSpeed
}

public var indeterminateAmplitude: Float {
Float(amplitude?(nil) ?? 1.0)
}

public var amplitudeForProgress: (Float) -> Float {
{ fraction in Float(amplitude?(Double(fraction)) ?? 1.0) }
}

public static func == (lhs: Material3WavyProgressConfiguration, rhs: Material3WavyProgressConfiguration) -> Bool {
guard lhs.isEnabled == rhs.isEnabled, lhs.wavelength == rhs.wavelength, lhs.waveSpeed == rhs.waveSpeed else {
return false
}
// Cannot compare closure values; treat as equal only when both are absent.
switch (lhs.amplitude, rhs.amplitude) {
case (nil, nil): return true
default: return false
}
}
}
#endif

// SKIP INSERT: @OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class)
// SKIP @bridge
public struct ProgressView : View, Renderable {
let value: Double?
Expand Down Expand Up @@ -111,25 +157,54 @@ public struct ProgressView : View, Renderable {
@Composable private func RenderLinearProgress(context: ComposeContext) {
let modifier = Modifier.fillWidth().then(context.modifier)
let color = EnvironmentValues.shared._tint?.colorImpl() ?? ProgressIndicatorDefaults.linearColor
if value == nil || total == nil {
LinearProgressIndicator(modifier: modifier, color: color)
let material3WavyProgressConfiguration = EnvironmentValues.shared._material3WavyProgress
if let wavyConfiguration = material3WavyProgressConfiguration, wavyConfiguration.isEnabled {
if let value, let total {
material3LinearWavyDeterminate(modifier: modifier, color: color, wavyConfiguration: wavyConfiguration, progress: { Float(value / total) })
} else {
material3LinearWavyIndeterminate(modifier: modifier, color: color, wavyConfiguration: wavyConfiguration)
}
} else if let value, let total {
LinearProgressIndicator(progress: { Float(value / total) }, modifier: modifier, color: color)
} else {
LinearProgressIndicator(progress: { Float(value! / total!) }, modifier: modifier, color: color)
LinearProgressIndicator(modifier: modifier, color: color)
}
}

@Composable private func material3LinearWavyIndeterminate(modifier: Modifier, color: androidx.compose.ui.graphics.Color, wavyConfiguration: Material3WavyProgressConfiguration) {
let wavelength = wavyConfiguration.wavelength.map { Float($0).dp } ?? WavyProgressIndicatorDefaults.LinearIndeterminateWavelength
let waveSpeed = wavyConfiguration.waveSpeed.map { Float($0).dp } ?? wavelength
LinearWavyProgressIndicator(modifier: modifier, color: color, amplitude: wavyConfiguration.indeterminateAmplitude, wavelength: wavelength, waveSpeed: waveSpeed)
}

@Composable private func material3LinearWavyDeterminate(modifier: Modifier, color: androidx.compose.ui.graphics.Color, wavyConfiguration: Material3WavyProgressConfiguration, progress: () -> Float) {
let wavelength = wavyConfiguration.wavelength.map { Float($0).dp } ?? WavyProgressIndicatorDefaults.LinearDeterminateWavelength
let waveSpeed = wavyConfiguration.waveSpeed.map { Float($0).dp } ?? wavelength
LinearWavyProgressIndicator(progress: progress, modifier: modifier, color: color, amplitude: wavyConfiguration.amplitudeForProgress, wavelength: wavelength, waveSpeed: waveSpeed)
}

@Composable private func RenderCircularProgress(context: ComposeContext) {
let color = EnvironmentValues.shared._tint?.colorImpl() ?? ProgressIndicatorDefaults.circularColor
// Reduce size to better match SwiftUI
let indicatorModifier = Modifier.size(20.dp)
let indicatorModifier = context.modifier.size(20.dp)
let material3WavyProgressConfiguration = EnvironmentValues.shared._material3WavyProgress
Box(modifier: context.modifier, contentAlignment: androidx.compose.ui.Alignment.Center) {
if value == nil || total == nil {
CircularProgressIndicator(modifier: indicatorModifier, color: color)
if let wavyConfiguration = material3WavyProgressConfiguration, wavyConfiguration.isEnabled {
let wavelength = wavyConfiguration.wavelength.map { Float($0).dp } ?? WavyProgressIndicatorDefaults.CircularWavelength
let waveSpeed = wavyConfiguration.waveSpeed.map { Float($0).dp } ?? wavelength
if let value, let total {
CircularWavyProgressIndicator(progress: { Float(value / total) }, modifier: indicatorModifier, color: color, amplitude: wavyConfiguration.amplitudeForProgress, wavelength: wavelength, waveSpeed: waveSpeed)
} else {
CircularWavyProgressIndicator(modifier: indicatorModifier, color: color, amplitude: wavyConfiguration.indeterminateAmplitude, wavelength: wavelength, waveSpeed: waveSpeed)
}
} else if let value, let total {
CircularProgressIndicator(progress: { Float(value / total) }, modifier: indicatorModifier, color: color)
} else {
CircularProgressIndicator(progress: { Float(value! / total!) }, modifier: indicatorModifier, color: color)
CircularProgressIndicator(modifier: indicatorModifier, color: color)
}
}
}

#else
public var body: some View {
stubView()
Expand Down Expand Up @@ -158,6 +233,18 @@ extension View {
#endif
}

#if SKIP
public func material3WavyProgress(wavy: Bool = true, amplitude: ((Double?) -> Double)? = nil, wavelength: Double? = nil, waveSpeed: Double? = nil) -> any View {
let wavyProgressConfiguration = Material3WavyProgressConfiguration(isEnabled: wavy, amplitude: amplitude, wavelength: wavelength, waveSpeed: waveSpeed)
return environment(\._material3WavyProgress, wavyProgressConfiguration, affectsEvaluate: false)
}

/// Convenience overload: constant amplitude for every progress value (same as `{ _ in fixedAmplitude }`).
public func material3WavyProgress(wavy: Bool = true, amplitude fixedAmplitude: Double, wavelength: Double? = nil, waveSpeed: Double? = nil) -> any View {
material3WavyProgress(wavy: wavy, amplitude: { _ in fixedAmplitude }, wavelength: wavelength, waveSpeed: waveSpeed)
}
#endif

// SKIP @bridge
public func progressViewStyle(bridgedStyle: Int) -> any View {
return progressViewStyle(ProgressViewStyle(rawValue: bridgedStyle))
Expand Down
5 changes: 5 additions & 0 deletions Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,11 @@ extension EnvironmentValues {
set { setBuiltinValue(key: "_progressViewStyle", value: newValue, defaultValue: { nil }) }
}

var _material3WavyProgress: Material3WavyProgressConfiguration? {
get { builtinValue(key: "_material3WavyProgress", defaultValue: { nil }) as! Material3WavyProgressConfiguration? }
set { setBuiltinValue(key: "_material3WavyProgress", value: newValue, defaultValue: { nil }) }
}

var _tabViewStyle: TabViewStyle? {
get { builtinValue(key: "_tabViewStyle", defaultValue: { nil }) as! TabViewStyle? }
set { setBuiltinValue(key: "_tabViewStyle", value: newValue, defaultValue: { nil }) }
Expand Down