Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 8aacebe

Browse files
committed
displayLink weak reference
1 parent c60dcce commit 8aacebe

File tree

3 files changed

+115
-78
lines changed

3 files changed

+115
-78
lines changed

widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/core/factory/ButtonWithIconViewFactory.kt

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ actual class ButtonWithIconViewFactory actual constructor(
9090
button.setTitle(title = processedText, forState = UIControlStateNormal)
9191
}
9292
}
93+
9394
else -> throw Exception("Not supported content type")
9495
}
9596

@@ -109,14 +110,17 @@ actual class ButtonWithIconViewFactory actual constructor(
109110
contentAttribute = UISemanticContentAttributeForceLeftToRight
110111
contentAlignment = UIControlContentHorizontalAlignmentLeft
111112
}
113+
112114
IconGravity.END -> {
113115
contentAttribute = UISemanticContentAttributeForceRightToLeft
114116
contentAlignment = UIControlContentHorizontalAlignmentLeft
115117
}
118+
116119
IconGravity.TEXT_START, null -> {
117120
contentAttribute = UISemanticContentAttributeForceLeftToRight
118121
contentAlignment = UIControlContentHorizontalAlignmentCenter
119122
}
123+
120124
IconGravity.TEXT_END -> {
121125
contentAttribute = UISemanticContentAttributeForceRightToLeft
122126
contentAlignment = UIControlContentHorizontalAlignmentCenter
@@ -127,27 +131,36 @@ actual class ButtonWithIconViewFactory actual constructor(
127131

128132
button.imageView?.let { button.bringSubviewToFront(it) }
129133

130-
var buttonWidth = 0.0
131-
// TODO remove this bad :(
132-
button.displayLink {
133-
val icPadding: Double = iconPadding?.toDouble() ?: 0.0
134+
setupLayoutUpdate(button, viewFactory = this)
135+
136+
return ViewBundle(
137+
view = button,
138+
size = size,
139+
margins = margins
140+
)
141+
}
134142

135-
val newButtonWidth = button.bounds.useContents { this.size.width }
136-
if (buttonWidth == newButtonWidth) return@displayLink
137-
buttonWidth = newButtonWidth
143+
private fun setupLayoutUpdate(button: UIButton, viewFactory: ButtonWithIconViewFactory) {
144+
button.displayLink(
145+
context = viewFactory,
146+
objectForSkipCheck = { it.bounds }
147+
) { button, viewFactory ->
148+
val icPadding: Double = viewFactory.iconPadding?.toDouble() ?: 0.0
149+
150+
val buttonWidth: CGFloat = button.bounds.useContents { this.size.width }
138151

139152
val iconWidth = button.imageView?.frame?.useContents { this.size.width } ?: 0.0
140153
val titleWidth = button.titleLabel?.frame?.useContents { this.size.width } ?: 0.0
141154

142-
val paddingTop = padding?.top?.toDouble() ?: 0.0
143-
var paddingLeft = padding?.start?.toDouble() ?: 0.0
144-
var paddingRight = padding?.end?.toDouble() ?: 0.0
145-
val paddingBottom = padding?.bottom?.toDouble() ?: 0.0
155+
val paddingTop = viewFactory.padding?.top?.toDouble() ?: 0.0
156+
var paddingLeft = viewFactory.padding?.start?.toDouble() ?: 0.0
157+
var paddingRight = viewFactory.padding?.end?.toDouble() ?: 0.0
158+
val paddingBottom = viewFactory.padding?.bottom?.toDouble() ?: 0.0
146159

147160
val titleLeftInset: CGFloat
148161
val titleRightInset: CGFloat
149162

150-
when (iconGravity) {
163+
when (viewFactory.iconGravity) {
151164
IconGravity.START -> {
152165
val inset = buttonWidth -
153166
iconWidth - titleWidth -
@@ -157,6 +170,7 @@ actual class ButtonWithIconViewFactory actual constructor(
157170
titleRightInset = -inset
158171
paddingRight += inset
159172
}
173+
160174
IconGravity.END -> {
161175
val inset = buttonWidth -
162176
iconWidth - titleWidth -
@@ -166,11 +180,13 @@ actual class ButtonWithIconViewFactory actual constructor(
166180
titleRightInset = inset
167181
paddingLeft += inset
168182
}
183+
169184
IconGravity.TEXT_START, null -> {
170185
titleLeftInset = icPadding
171186
titleRightInset = -icPadding
172187
paddingRight += icPadding
173188
}
189+
174190
IconGravity.TEXT_END -> {
175191
titleLeftInset = -icPadding
176192
titleRightInset = icPadding
@@ -191,11 +207,5 @@ actual class ButtonWithIconViewFactory actual constructor(
191207
right = paddingRight
192208
)
193209
}
194-
195-
return ViewBundle(
196-
view = button,
197-
size = size,
198-
margins = margins
199-
)
200210
}
201211
}

widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/core/utils/BackgroundExt.kt

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ import dev.icerock.moko.widgets.core.style.background.Direction
1010
import dev.icerock.moko.widgets.core.style.background.Fill
1111
import dev.icerock.moko.widgets.core.style.state.PressableState
1212
import kotlinx.cinterop.useContents
13+
import platform.CoreGraphics.CGFloat
1314
import platform.CoreGraphics.CGPointMake
1415
import platform.CoreGraphics.CGRectMake
15-
import platform.Foundation.NSRunLoop
16-
import platform.Foundation.NSRunLoopCommonModes
17-
import platform.QuartzCore.CADisplayLink
1816
import platform.QuartzCore.CAGradientLayer
1917
import platform.QuartzCore.CALayer
2018
import platform.QuartzCore.CATransaction
2119
import platform.UIKit.UIButton
2220
import platform.UIKit.UIColor
21+
import platform.UIKit.UIControl
2322
import platform.UIKit.UIView
2423
import platform.UIKit.adjustsImageWhenDisabled
2524
import platform.UIKit.adjustsImageWhenHighlighted
2625
import platform.UIKit.backgroundColor
27-
import platform.UIKit.window
2826

2927
@Suppress("MagicNumber", "ComplexMethod")
3028
fun Background<out Fill>.caLayer(): CALayer {
@@ -35,6 +33,7 @@ fun Background<out Fill>.caLayer(): CALayer {
3533
is Fill.Solid -> backgroundLayer = CALayer().apply {
3634
backgroundColor = fill.color.toUIColor().CGColor
3735
}
36+
3837
is Fill.Gradient -> {
3938
backgroundLayer = CAGradientLayer().apply {
4039
colors = cgColors(fill.colors.map {
@@ -58,6 +57,7 @@ fun Background<out Fill>.caLayer(): CALayer {
5857
endPoint = end
5958
}
6059
}
60+
6161
null -> {
6262
backgroundLayer = CALayer()
6363
}
@@ -84,56 +84,62 @@ fun UIButton.applyStateBackgroundIfNeeded(background: PressableState<Background<
8484
adjustsImageWhenDisabled = false
8585
adjustsImageWhenHighlighted = false
8686

87-
val normalBg = background.normal.caLayer().also {
88-
layer.addSublayer(it)
89-
}
90-
val disabledBg = background.disabled.caLayer().also {
91-
layer.addSublayer(it)
92-
}
93-
val pressedBg = background.pressed.caLayer().also {
94-
layer.addSublayer(it)
95-
}
96-
97-
fun updateLayers() {
98-
if (!isEnabled()) {
99-
disabledBg.opacity = 1.0f
100-
normalBg.opacity = 0f
101-
pressedBg.opacity = 0f
102-
return
87+
val stateLayers = StateLayers(
88+
normal = background.normal.caLayer().also {
89+
layer.addSublayer(it)
90+
},
91+
disabled = background.disabled.caLayer().also {
92+
layer.addSublayer(it)
93+
},
94+
pressed = background.pressed.caLayer().also {
95+
layer.addSublayer(it)
10396
}
97+
)
10498

105-
if (isHighlighted()) {
106-
pressedBg.opacity = 1.0f
107-
normalBg.opacity = 0f
108-
} else {
109-
normalBg.opacity = 1.0f
110-
pressedBg.opacity = 0f
111-
}
112-
disabledBg.opacity = 0f
113-
}
99+
stateLayers.update(this)
114100

115-
updateLayers()
101+
this.displayLink(
102+
context = stateLayers,
103+
objectForSkipCheck = { it.layer.bounds }
104+
) { button, stateLayers ->
105+
val (width: CGFloat, height: CGFloat) = button.layer.bounds.useContents {
106+
this.size.width to this.size.height
107+
}
116108

117-
// FIXME memoryleak, perfomance problem !!!
118-
var link: CADisplayLink? = null
109+
CATransaction.begin()
110+
CATransaction.setDisableActions(true)
119111

120-
link = displayLink {
121-
if (window != null) {
122-
val (width, height) = layer.bounds.useContents { size.width to size.height }
112+
stateLayers.normal.frame = CGRectMake(0.0, 0.0, width, height)
113+
stateLayers.disabled.frame = CGRectMake(0.0, 0.0, width, height)
114+
stateLayers.pressed.frame = CGRectMake(0.0, 0.0, width, height)
123115

124-
CATransaction.begin()
125-
CATransaction.setDisableActions(true)
116+
stateLayers.update(button)
126117

127-
normalBg.frame = CGRectMake(0.0, 0.0, width, height)
128-
disabledBg.frame = CGRectMake(0.0, 0.0, width, height)
129-
pressedBg.frame = CGRectMake(0.0, 0.0, width, height)
118+
CATransaction.commit()
119+
}
120+
}
130121

131-
updateLayers()
122+
private data class StateLayers(
123+
val normal: CALayer,
124+
val disabled: CALayer,
125+
val pressed: CALayer
126+
) {
127+
fun update(control: UIControl) {
128+
if (!control.isEnabled()) {
129+
disabled.opacity = 1.0f
130+
normal.opacity = 0f
131+
pressed.opacity = 0f
132+
return
133+
}
132134

133-
CATransaction.commit()
135+
if (control.isHighlighted()) {
136+
pressed.opacity = 1.0f
137+
normal.opacity = 0f
134138
} else {
135-
link?.removeFromRunLoop(NSRunLoop.currentRunLoop, NSRunLoopCommonModes)
139+
normal.opacity = 1.0f
140+
pressed.opacity = 0f
136141
}
142+
disabled.opacity = 0f
137143
}
138144
}
139145

@@ -156,24 +162,22 @@ fun UIView.applyBackgroundIfNeeded(background: Background<out Fill>?) {
156162

157163
this.backgroundColor = UIColor.clearColor
158164

159-
val bgLayer = background.caLayer()
165+
val bgLayer: CALayer = background.caLayer()
160166
layer.insertSublayer(bgLayer, 0U)
161167

162-
// FIXME memoryleak, perfomance problem !!!
163-
var link: CADisplayLink? = null
164-
165-
link = displayLink {
166-
if (window != null) {
167-
val (width, height) = layer.bounds.useContents { size.width to size.height }
168+
this.displayLink(
169+
context = bgLayer,
170+
objectForSkipCheck = { it.layer.bounds }
171+
) { view, bgLayer ->
172+
val (width: CGFloat, height: CGFloat) = view.layer.bounds.useContents {
173+
this.size.width to this.size.height
174+
}
168175

169-
CATransaction.begin()
170-
CATransaction.setDisableActions(true)
176+
CATransaction.begin()
177+
CATransaction.setDisableActions(true)
171178

172-
bgLayer.frame = CGRectMake(0.0, 0.0, width, height)
179+
bgLayer.frame = CGRectMake(0.0, 0.0, width, height)
173180

174-
CATransaction.commit()
175-
} else {
176-
link?.removeFromRunLoop(NSRunLoop.currentRunLoop, NSRunLoopCommonModes)
177-
}
181+
CATransaction.commit()
178182
}
179183
}

widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/core/utils/UIControlExt.kt

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import platform.QuartzCore.CADisplayLink
1313
import platform.UIKit.UIControl
1414
import platform.UIKit.UIControlEvents
1515
import platform.UIKit.UIGestureRecognizer
16+
import platform.UIKit.UIView
1617
import platform.darwin.NSObject
1718
import platform.objc.OBJC_ASSOCIATION_RETAIN
1819
import platform.objc.objc_setAssociatedObject
20+
import kotlin.native.ref.WeakReference
1921

2022
fun UIControl.setEventHandler(controlEvent: UIControlEvents, action: () -> Unit) {
2123
val target = LambdaTarget(action)
@@ -50,10 +52,31 @@ fun UIGestureRecognizer.setHandler(action: () -> Unit) {
5052
)
5153
}
5254

53-
fun NSObject.displayLink(action: () -> Unit): CADisplayLink {
54-
val target = LambdaTarget(action)
55+
fun <T : Any, CTX> T.displayLink(
56+
context: CTX,
57+
objectForSkipCheck: (T) -> Any,
58+
action: (T, CTX) -> Unit
59+
) {
60+
val ref: WeakReference<T> = WeakReference(this)
61+
62+
var displayLink: CADisplayLink? = null
63+
64+
var oldState: Any = objectForSkipCheck(this)
65+
val target = LambdaTarget {
66+
val strongRef: T? = ref.get()
67+
if (strongRef == null) {
68+
displayLink?.removeFromRunLoop(NSRunLoop.currentRunLoop, NSRunLoopCommonModes)
69+
displayLink = null
70+
return@LambdaTarget
71+
}
72+
73+
val newState: Any = objectForSkipCheck(strongRef)
74+
if (newState == oldState) return@LambdaTarget
75+
76+
action(strongRef, context)
77+
}
5578

56-
return CADisplayLink.displayLinkWithTarget(
79+
CADisplayLink.displayLinkWithTarget(
5780
target = target,
5881
selector = NSSelectorFromString("displayLink:")
5982
).apply {

0 commit comments

Comments
 (0)