added range support for slider
@@ -11277,6 +11277,20 @@
|
||||
"client/ios/Tests/reference_snapshots/div-slider/fixed_size_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/fixed_size_414@3x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/match_parent_size_375@2x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/match_parent_size_375@2x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/match_parent_size_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/match_parent_size_414@3x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step0.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step0.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step1.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step1.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step2.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step2.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step3.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step3.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step4.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step4.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step5.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step5.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step0.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step0.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step1.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step1.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step2.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step2.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step3.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step3.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step4.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step4.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step5.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step5.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/rtl_slider_375@2x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/rtl_slider_375@2x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/rtl_slider_414@3x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/rtl_slider_414@3x.png",
|
||||
"client/ios/Tests/reference_snapshots/div-slider/secondary_thumb_on_left_375@2x.png":"divkit/public/client/ios/Tests/reference_snapshots/div-slider/secondary_thumb_on_left_375@2x.png",
|
||||
|
||||
@@ -19,16 +19,42 @@ extension DivSlider: DivBlockModeling {
|
||||
|
||||
private func makeBaseBlock(context: DivBlockModelingContext) throws -> Block {
|
||||
let expressionResolver = context.expressionResolver
|
||||
|
||||
let firstThumbValue: Binding<Int> = thumbValueVariable.flatMap {
|
||||
context.makeBinding(variableName: $0, defaultValue: 0)
|
||||
} ?? .zero
|
||||
let firstThumb = SliderModel.ThumbModel(
|
||||
block: makeThumbBlock(
|
||||
thumb: thumbStyle
|
||||
.makeBlock(context: context, corners: .all),
|
||||
textBlock: thumbTextStyle?.makeThumbTextBlock(
|
||||
context: context,
|
||||
value: firstThumbValue.value
|
||||
),
|
||||
textOffset: thumbTextStyle.flatMap {
|
||||
CGPoint(
|
||||
x: $0.offset?.x.resolveValue(expressionResolver) ?? 0,
|
||||
y: $0.offset?.y.resolveValue(expressionResolver) ?? 0
|
||||
)
|
||||
} ?? .zero
|
||||
),
|
||||
value: firstThumbValue,
|
||||
size: CGSize(
|
||||
width: thumbStyle.getWidth(context: context),
|
||||
height: thumbStyle.getHeight(context: context)
|
||||
),
|
||||
offsetX: thumbTextStyle?.offset?.x.resolveValue(expressionResolver) ?? 0,
|
||||
offsetY: thumbTextStyle?.offset?.y.resolveValue(expressionResolver) ?? 0
|
||||
)
|
||||
let secondThumb: SliderModel.ThumbModel?
|
||||
if let thumbSecondaryStyle = thumbSecondaryStyle,
|
||||
let thumbSecondaryValueVariable = thumbSecondaryValueVariable {
|
||||
if let thumbSecondaryValueVariable = thumbSecondaryValueVariable {
|
||||
let secondThumbValue = context.makeBinding(
|
||||
variableName: thumbSecondaryValueVariable,
|
||||
defaultValue: 0
|
||||
)
|
||||
secondThumb = SliderModel.ThumbModel(
|
||||
block: makeThumbBlock(
|
||||
thumb: thumbSecondaryStyle
|
||||
thumb: thumbSecondaryStyle?.makeBlock(context: context, corners: .all) ?? thumbStyle
|
||||
.makeBlock(context: context, corners: .all),
|
||||
textBlock: (thumbSecondaryTextStyle ?? thumbTextStyle)?.makeThumbTextBlock(
|
||||
context: context,
|
||||
@@ -43,8 +69,10 @@ extension DivSlider: DivBlockModeling {
|
||||
),
|
||||
value: secondThumbValue,
|
||||
size: CGSize(
|
||||
width: thumbSecondaryStyle.getWidth(context: context),
|
||||
height: thumbSecondaryStyle.getHeight(context: context)
|
||||
width: thumbSecondaryStyle?.getWidth(context: context) ?? thumbStyle
|
||||
.getWidth(context: context),
|
||||
height: thumbSecondaryStyle?.getHeight(context: context) ?? thumbStyle
|
||||
.getHeight(context: context)
|
||||
),
|
||||
offsetX: thumbSecondaryTextStyle?.offset?.x.resolveValue(expressionResolver) ?? 0,
|
||||
offsetY: thumbSecondaryTextStyle?.offset?.y.resolveValue(expressionResolver) ?? 0
|
||||
@@ -53,90 +81,117 @@ extension DivSlider: DivBlockModeling {
|
||||
secondThumb = nil
|
||||
}
|
||||
|
||||
let firstThumbValue: Binding<Int> = thumbValueVariable.flatMap {
|
||||
context.makeBinding(variableName: $0, defaultValue: 0)
|
||||
} ?? .zero
|
||||
|
||||
let activeMark = makeRoundedRectangle(with: tickMarkActiveStyle, resolver: expressionResolver)
|
||||
let inactiveMark = makeRoundedRectangle(
|
||||
with: tickMarkInactiveStyle,
|
||||
resolver: expressionResolver
|
||||
)
|
||||
|
||||
let minValue = resolveMinValue(expressionResolver)
|
||||
let maxValue = resolveMaxValue(expressionResolver)
|
||||
|
||||
let marksConfiguration = MarksConfiguration(
|
||||
minValue: CGFloat(resolveMinValue(expressionResolver)),
|
||||
maxValue: CGFloat(resolveMaxValue(expressionResolver)),
|
||||
minValue: CGFloat(minValue),
|
||||
maxValue: CGFloat(maxValue),
|
||||
activeMark: activeMark ?? .empty,
|
||||
inactiveMark: inactiveMark ?? .empty,
|
||||
layoutDirection: context.layoutDirection
|
||||
)
|
||||
|
||||
let sliderRanges = makeRanges(ranges, with: context)
|
||||
|
||||
let sliderModel = SliderModel(
|
||||
firstThumb: SliderModel.ThumbModel(
|
||||
block: makeThumbBlock(
|
||||
thumb: thumbStyle
|
||||
.makeBlock(context: context, corners: .all),
|
||||
textBlock: thumbTextStyle?.makeThumbTextBlock(
|
||||
context: context,
|
||||
value: firstThumbValue.value
|
||||
),
|
||||
textOffset: thumbTextStyle.flatMap {
|
||||
CGPoint(
|
||||
x: $0.offset?.x.resolveValue(expressionResolver) ?? 0,
|
||||
y: $0.offset?.y.resolveValue(expressionResolver) ?? 0
|
||||
)
|
||||
} ?? .zero
|
||||
),
|
||||
value: firstThumbValue,
|
||||
size: CGSize(
|
||||
width: thumbStyle.getWidth(context: context),
|
||||
height: thumbStyle.getHeight(context: context)
|
||||
),
|
||||
offsetX: thumbTextStyle?.offset?.x.resolveValue(expressionResolver) ?? 0,
|
||||
offsetY: thumbTextStyle?.offset?.y.resolveValue(expressionResolver) ?? 0
|
||||
),
|
||||
firstThumb: firstThumb,
|
||||
secondThumb: secondThumb,
|
||||
activeMarkModel: tickMarkActiveStyle.flatMap {
|
||||
SliderModel.MarkModel(
|
||||
block: $0.makeBlock(context: context, corners: .all),
|
||||
size: CGSize(
|
||||
width: $0.getWidth(context: context),
|
||||
height: $0.getHeight(context: context)
|
||||
)
|
||||
)
|
||||
},
|
||||
inactiveMarkModel: tickMarkInactiveStyle.flatMap {
|
||||
SliderModel.MarkModel(
|
||||
block: $0.makeBlock(context: context, corners: .all),
|
||||
size: CGSize(
|
||||
width: $0.getWidth(context: context),
|
||||
height: $0.getHeight(context: context)
|
||||
)
|
||||
)
|
||||
},
|
||||
minValue: resolveMinValue(expressionResolver),
|
||||
maxValue: resolveMaxValue(expressionResolver),
|
||||
activeTrack: self.trackActiveStyle.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: .all
|
||||
),
|
||||
inactiveTrack: self.trackInactiveStyle.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: .all
|
||||
),
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
marksConfiguration: marksConfiguration,
|
||||
ranges: sliderRanges,
|
||||
layoutDirection: context.layoutDirection
|
||||
)
|
||||
|
||||
let width = context.override(width: width)
|
||||
let height = context.override(height: height)
|
||||
|
||||
return SliderBlock(
|
||||
sliderModel: sliderModel,
|
||||
widthTrait: width.makeLayoutTrait(with: expressionResolver),
|
||||
heightTrait: height.makeLayoutTrait(with: expressionResolver)
|
||||
)
|
||||
}
|
||||
|
||||
private func makeRanges(
|
||||
_ ranges: [DivSlider.Range]?,
|
||||
with context: DivBlockModelingContext
|
||||
) -> [SliderModel.RangeModel] {
|
||||
let expressionResolver = context.expressionResolver
|
||||
|
||||
let minValue = resolveMinValue(expressionResolver)
|
||||
let maxValue = resolveMaxValue(expressionResolver)
|
||||
|
||||
var sliderRanges: [SliderModel.RangeModel] = (ranges ?? []).map { range in
|
||||
SliderModel.RangeModel(
|
||||
start: range.resolveStart(expressionResolver) ?? minValue,
|
||||
end: range.resolveEnd(expressionResolver) ?? maxValue,
|
||||
margins: range.margins.makeEdgeInsets(context: context),
|
||||
activeTrack: range.trackActiveStyle?.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: .all
|
||||
) ?? trackActiveStyle.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: .all
|
||||
),
|
||||
inactiveTrack: range.trackInactiveStyle?.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: .all
|
||||
) ?? trackInactiveStyle.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: .all
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let makeBasicRange: (Int, Int, CGRect.Corners) -> SliderModel.RangeModel = { [self] in
|
||||
SliderModel.RangeModel(
|
||||
start: $0,
|
||||
end: $1,
|
||||
margins: EdgeInsets.zero,
|
||||
activeTrack: trackActiveStyle.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: $2
|
||||
),
|
||||
inactiveTrack: trackInactiveStyle.makeBlock(
|
||||
context: context,
|
||||
widthTrait: .resizable,
|
||||
corners: $2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
sliderRanges.sort { $0.start < $1.start }
|
||||
|
||||
var lastRangeEnd = minValue
|
||||
for (index, range) in sliderRanges.enumerated() {
|
||||
if range.start != lastRangeEnd {
|
||||
let corner: CGRect.Corners = index == 0 ? .left : .all
|
||||
sliderRanges.append(makeBasicRange(lastRangeEnd, range.start, corner))
|
||||
}
|
||||
lastRangeEnd = range.end
|
||||
}
|
||||
if lastRangeEnd != maxValue {
|
||||
let corner: CGRect.Corners = sliderRanges.count == 0 ? .all : .right
|
||||
sliderRanges.append(makeBasicRange(lastRangeEnd, maxValue, corner))
|
||||
}
|
||||
|
||||
sliderRanges.sort { $0.start < $1.start }
|
||||
|
||||
return sliderRanges
|
||||
}
|
||||
}
|
||||
|
||||
private func makeThumbBlock(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseTinyPublic
|
||||
import Foundation
|
||||
|
||||
public struct MarksConfiguration {
|
||||
public struct MarksConfiguration: Equatable {
|
||||
let minValue: CGFloat
|
||||
let maxValue: CGFloat
|
||||
var horizontalInset: CGFloat = 0
|
||||
@@ -33,7 +33,7 @@ public struct MarksConfiguration {
|
||||
}
|
||||
|
||||
extension MarksConfiguration {
|
||||
public struct RoundedRectangle {
|
||||
public struct RoundedRectangle: Equatable {
|
||||
var size: CGSize
|
||||
let cornerRadius: CGFloat
|
||||
let color: Color
|
||||
@@ -62,4 +62,13 @@ extension MarksConfiguration {
|
||||
borderColor: .clear
|
||||
)
|
||||
}
|
||||
|
||||
public static func ==(lhs: MarksConfiguration, rhs: MarksConfiguration) -> Bool {
|
||||
lhs.minValue == rhs.minValue &&
|
||||
lhs.maxValue == rhs.maxValue &&
|
||||
lhs.horizontalInset == rhs.horizontalInset &&
|
||||
lhs.inactiveMark == rhs.inactiveMark &&
|
||||
lhs.activeMark == rhs.activeMark &&
|
||||
lhs.layoutDirection == rhs.layoutDirection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,33 +64,63 @@ public struct SliderModel: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct RangeModel: Equatable {
|
||||
public let start: Int
|
||||
public let end: Int
|
||||
public let margins: EdgeInsets
|
||||
public let activeTrack: Block
|
||||
public let inactiveTrack: Block
|
||||
|
||||
public init(
|
||||
start: Int,
|
||||
end: Int,
|
||||
margins: EdgeInsets,
|
||||
activeTrack: Block,
|
||||
inactiveTrack: Block
|
||||
) {
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.margins = margins
|
||||
self.activeTrack = activeTrack
|
||||
self.inactiveTrack = inactiveTrack
|
||||
}
|
||||
|
||||
public static func ==(lhs: SliderModel.RangeModel, rhs: SliderModel.RangeModel) -> Bool {
|
||||
lhs.start == rhs.start && lhs.end == rhs.end && lhs.activeTrack == rhs.activeTrack && lhs
|
||||
.inactiveTrack == rhs.inactiveTrack && lhs.margins == rhs.margins
|
||||
}
|
||||
}
|
||||
|
||||
public var firstThumb: ThumbModel
|
||||
public var secondThumb: ThumbModel?
|
||||
|
||||
public let minValue: Int
|
||||
public let maxValue: Int
|
||||
public let activeMarkModel: MarkModel?
|
||||
public let inactiveMarkModel: MarkModel?
|
||||
public let activeTrack: Block
|
||||
public let inactiveTrack: Block
|
||||
public let marksConfiguration: MarksConfiguration
|
||||
public let ranges: [RangeModel]
|
||||
public let layoutDirection: UserInterfaceLayoutDirection
|
||||
|
||||
public var valueRange: Int {
|
||||
maxValue - minValue
|
||||
}
|
||||
|
||||
public var sliderHeight: CGFloat {
|
||||
max(
|
||||
activeMarkModel?.size.height ?? 0,
|
||||
inactiveMarkModel?.size.height ?? 0,
|
||||
marksConfiguration.activeMark.size.height,
|
||||
marksConfiguration.inactiveMark.size.height,
|
||||
firstThumb.size.height,
|
||||
secondThumb?.size.height ?? 0,
|
||||
activeTrack.intrinsicContentHeight(forWidth: .infinity),
|
||||
inactiveTrack.intrinsicContentHeight(forWidth: .infinity)
|
||||
ranges.map { max(
|
||||
$0.activeTrack.intrinsicContentHeight(forWidth: .infinity),
|
||||
$0.inactiveTrack.intrinsicContentHeight(forWidth: .infinity)
|
||||
) }.max() ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
public var sliderIntrinsicWidth: CGFloat {
|
||||
let maxMarkWidth = max(
|
||||
activeMarkModel?.size.width ?? 0,
|
||||
inactiveMarkModel?.size.width ?? 0
|
||||
marksConfiguration.activeMark.size.width,
|
||||
marksConfiguration.inactiveMark.size.width
|
||||
)
|
||||
let maxThumbWidth = max(firstThumb.size.width, secondThumb?.size.width ?? 0)
|
||||
return CGFloat(maxValue - minValue + 1) * maxMarkWidth + max(0, maxThumbWidth - maxMarkWidth)
|
||||
@@ -106,8 +136,8 @@ public struct SliderModel: Equatable {
|
||||
|
||||
public var horizontalInset: CGFloat {
|
||||
max(
|
||||
activeMarkModel?.size.width ?? 0,
|
||||
inactiveMarkModel?.size.width ?? 0,
|
||||
marksConfiguration.activeMark.size.width,
|
||||
marksConfiguration.inactiveMark.size.width,
|
||||
firstThumb.size.width,
|
||||
secondThumb?.size.width ?? 0
|
||||
)
|
||||
@@ -116,24 +146,18 @@ public struct SliderModel: Equatable {
|
||||
public init(
|
||||
firstThumb: ThumbModel,
|
||||
secondThumb: ThumbModel? = nil,
|
||||
activeMarkModel: MarkModel?,
|
||||
inactiveMarkModel: MarkModel?,
|
||||
minValue: Int,
|
||||
maxValue: Int,
|
||||
activeTrack: Block,
|
||||
inactiveTrack: Block,
|
||||
marksConfiguration: MarksConfiguration,
|
||||
ranges: [RangeModel],
|
||||
layoutDirection: UserInterfaceLayoutDirection = .leftToRight
|
||||
) {
|
||||
self.firstThumb = firstThumb
|
||||
self.secondThumb = secondThumb
|
||||
self.minValue = minValue
|
||||
self.maxValue = maxValue
|
||||
self.activeMarkModel = activeMarkModel
|
||||
self.inactiveMarkModel = inactiveMarkModel
|
||||
self.activeTrack = activeTrack
|
||||
self.inactiveTrack = inactiveTrack
|
||||
self.marksConfiguration = marksConfiguration
|
||||
self.ranges = ranges
|
||||
self.layoutDirection = layoutDirection
|
||||
}
|
||||
|
||||
@@ -141,13 +165,10 @@ public struct SliderModel: Equatable {
|
||||
SliderModel(
|
||||
firstThumb: .empty,
|
||||
secondThumb: nil,
|
||||
activeMarkModel: nil,
|
||||
inactiveMarkModel: nil,
|
||||
minValue: 0,
|
||||
maxValue: 0,
|
||||
activeTrack: EmptyBlock.zeroSized,
|
||||
inactiveTrack: EmptyBlock.zeroSized,
|
||||
marksConfiguration: .empty,
|
||||
ranges: [],
|
||||
layoutDirection: .leftToRight
|
||||
)
|
||||
}
|
||||
@@ -157,7 +178,7 @@ public struct SliderModel: Equatable {
|
||||
lhs.secondThumb == rhs.secondThumb &&
|
||||
lhs.maxValue == rhs.maxValue &&
|
||||
lhs.minValue == rhs.minValue &&
|
||||
lhs.inactiveMarkModel == rhs.inactiveMarkModel &&
|
||||
lhs.activeMarkModel == rhs.activeMarkModel
|
||||
lhs.marksConfiguration == rhs.marksConfiguration &&
|
||||
lhs.ranges == rhs.ranges
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,20 +28,20 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf {
|
||||
}
|
||||
self.sliderModel = sliderModel
|
||||
|
||||
inactiveTrack = sliderModel.inactiveTrack.reuse(
|
||||
inactiveTrack,
|
||||
inactiveRangeTracks = inactiveRangeTracks.reused(
|
||||
with: sliderModel.ranges.map { $0.inactiveTrack },
|
||||
attachTo: inactiveTrackView,
|
||||
observer: observer,
|
||||
overscrollDelegate: overscrollDelegate,
|
||||
renderingDelegate: renderingDelegate,
|
||||
superview: sliderBackgroundView
|
||||
renderingDelegate: renderingDelegate
|
||||
)
|
||||
|
||||
activeTrack = sliderModel.activeTrack.reuse(
|
||||
activeTrack,
|
||||
activeRangeTracks = activeRangeTracks.reused(
|
||||
with: sliderModel.ranges.map { $0.activeTrack },
|
||||
attachTo: activeTrackView,
|
||||
observer: observer,
|
||||
overscrollDelegate: overscrollDelegate,
|
||||
renderingDelegate: renderingDelegate,
|
||||
superview: sliderBackgroundView
|
||||
renderingDelegate: renderingDelegate
|
||||
)
|
||||
|
||||
firstThumb = sliderModel.firstThumb.block.reuse(
|
||||
@@ -113,33 +113,24 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf {
|
||||
}
|
||||
|
||||
private var pointWidth: CGFloat {
|
||||
let stripeCount = sliderModel.maxValue - sliderModel.minValue
|
||||
let width = bounds.width - sliderModel.horizontalInset - max(
|
||||
abs(sliderModel.firstThumb.offsetX),
|
||||
abs(sliderModel.secondThumb?.offsetX ?? 0)
|
||||
)
|
||||
if stripeCount == 0 {
|
||||
if sliderModel.valueRange == 0 {
|
||||
return width
|
||||
} else {
|
||||
return CGFloat(width) / CGFloat(stripeCount)
|
||||
return CGFloat(width) / CGFloat(sliderModel.valueRange)
|
||||
}
|
||||
}
|
||||
|
||||
private var firstThumb: BlockView?
|
||||
private var secondThumb: BlockView?
|
||||
private var activeTrack: BlockView?
|
||||
private var inactiveTrack: BlockView?
|
||||
private lazy var marksView: MarksView = {
|
||||
let view = MarksView()
|
||||
self.insertSubview(view, at: 1)
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var sliderBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
self.insertSubview(view, at: 0)
|
||||
return view
|
||||
}()
|
||||
private var activeRangeTracks: [BlockView] = []
|
||||
private var inactiveRangeTracks: [BlockView] = []
|
||||
private lazy var inactiveTrackView = makeAndInsertView(UIView(), at: 0)
|
||||
private lazy var activeTrackView = makeAndInsertView(UIView(), at: 1)
|
||||
private lazy var marksView = makeAndInsertView(MarksView(), at: 2)
|
||||
|
||||
private let recognizer = UILongPressGestureRecognizer()
|
||||
private var activeThumb: SliderThumbNumber = .first
|
||||
@@ -157,6 +148,12 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func makeAndInsertView<T: UIView>(_ view: T, at position: Int) -> T {
|
||||
view.clipsToBounds = true
|
||||
self.insertSubview(view, at: position)
|
||||
return view
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ recognizer: UILongPressGestureRecognizer) {
|
||||
let location = recognizer.location(in: self)
|
||||
|
||||
@@ -166,13 +163,13 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf {
|
||||
currentValue = clamp(
|
||||
(location.x - sliderModel.horizontalInset / 2) / pointWidth,
|
||||
min: 0.0,
|
||||
max: CGFloat(sliderModel.maxValue - sliderModel.minValue)
|
||||
max: CGFloat(sliderModel.valueRange)
|
||||
) + CGFloat(sliderModel.minValue)
|
||||
case .rightToLeft:
|
||||
currentValue = CGFloat(sliderModel.maxValue) - clamp(
|
||||
(location.x - sliderModel.horizontalInset / 2) / pointWidth,
|
||||
min: 0.0,
|
||||
max: CGFloat(sliderModel.maxValue - sliderModel.minValue)
|
||||
max: CGFloat(sliderModel.valueRange)
|
||||
)
|
||||
@unknown default:
|
||||
assertionFailure("Unknown layoutDirection (UserInterfaceLayoutDirection)")
|
||||
@@ -285,28 +282,66 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf {
|
||||
)
|
||||
|
||||
marksView.configuration.horizontalInset = sliderModel.horizontalInset
|
||||
marksView.frame = CGRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: bounds
|
||||
.width - max(
|
||||
abs(sliderModel.firstThumb.offsetX),
|
||||
abs(sliderModel.secondThumb?.offsetX ?? 0)
|
||||
),
|
||||
height: bounds.height
|
||||
configureSliderView(marksView)
|
||||
|
||||
configureSliderView(inactiveTrackView, with: sliderModel.horizontalInset)
|
||||
configureSliderView(activeTrackView, with: sliderModel.horizontalInset)
|
||||
|
||||
configureRangeViews(
|
||||
inactiveRangeTracks,
|
||||
with: sliderModel.ranges,
|
||||
in: inactiveTrackView,
|
||||
isActive: false
|
||||
)
|
||||
configureRangeViews(
|
||||
activeRangeTracks,
|
||||
with: sliderModel.ranges,
|
||||
in: activeTrackView,
|
||||
isActive: true
|
||||
)
|
||||
|
||||
sliderBackgroundView.frame = CGRect(
|
||||
x: 0,
|
||||
configureTracks(
|
||||
progressFirstThumb: firstThumbProgress,
|
||||
progressSecondThumb: sliderModel
|
||||
.secondThumb != nil ? secondThumbProgress : CGFloat(sliderModel.minValue)
|
||||
)
|
||||
}
|
||||
|
||||
private func configureRangeViews(
|
||||
_ rangeViews: [UIView],
|
||||
with models: [SliderModel.RangeModel],
|
||||
in parentView: UIView,
|
||||
isActive: Bool
|
||||
) {
|
||||
zip(rangeViews, models).forEach { view, model in
|
||||
let rangeOfValues = CGFloat(model.end - model.start) / CGFloat(sliderModel.valueRange)
|
||||
let rangeWidth = parentView.frame
|
||||
.width * rangeOfValues - (model.margins.left + model.margins.right)
|
||||
let rangeHeight = isActive ? model.activeTrack
|
||||
.intrinsicContentHeight(forWidth: rangeWidth) : model.inactiveTrack
|
||||
.intrinsicContentHeight(forWidth: rangeWidth)
|
||||
view.frame = CGRect(
|
||||
x: CGFloat(model.start - sliderModel.minValue) / CGFloat(sliderModel.valueRange) *
|
||||
inactiveTrackView.frame.width + model.margins.left,
|
||||
y: (sliderModel.sliderHeight - rangeHeight) / 2,
|
||||
width: rangeWidth,
|
||||
height: rangeHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureSliderView(_ view: UIView, with horizontalInset: CGFloat = 0) {
|
||||
view.frame = CGRect(
|
||||
x: horizontalInset / 2,
|
||||
y: 0,
|
||||
width: bounds
|
||||
.width - max(
|
||||
abs(sliderModel.firstThumb.offsetX),
|
||||
abs(sliderModel.secondThumb?.offsetX ?? 0)
|
||||
),
|
||||
) - horizontalInset,
|
||||
height: sliderModel.sliderHeight
|
||||
)
|
||||
sliderBackgroundView.center = bounds.center
|
||||
view.center = bounds.center
|
||||
.movingX(
|
||||
by:
|
||||
(
|
||||
@@ -319,55 +354,39 @@ final class SliderView: BlockView, VisibleBoundsTrackingLeaf {
|
||||
-(sliderModel.secondThumb?.offsetX ?? 0) / 2
|
||||
: (sliderModel.secondThumb?.offsetX ?? 0) / 2
|
||||
)
|
||||
|
||||
configureTracks(
|
||||
progressFirstThumb: firstThumbProgress,
|
||||
progressSecondThumb: sliderModel
|
||||
.secondThumb != nil ? secondThumbProgress : CGFloat(sliderModel.minValue)
|
||||
)
|
||||
}
|
||||
|
||||
private func configureTracks(
|
||||
progressFirstThumb: CGFloat,
|
||||
progressSecondThumb: CGFloat
|
||||
) {
|
||||
let makeModifiedSliderViewBounds: (CGFloat) -> CGRect = { [self] trackHeight in
|
||||
sliderBackgroundView.bounds.insetBy(
|
||||
dx: sliderModel.horizontalInset / 2,
|
||||
dy: (
|
||||
sliderModel.sliderHeight - trackHeight
|
||||
) / 2
|
||||
)
|
||||
}
|
||||
|
||||
inactiveTrack?.frame = makeModifiedSliderViewBounds(
|
||||
sliderModel.inactiveTrack
|
||||
.intrinsicContentHeight(
|
||||
forWidth: .infinity
|
||||
)
|
||||
)
|
||||
|
||||
activeTrack?.frame = modified(makeModifiedSliderViewBounds(
|
||||
sliderModel.activeTrack
|
||||
.intrinsicContentHeight(
|
||||
forWidth: .infinity
|
||||
)
|
||||
)) {
|
||||
let progressFirstThumbWidth =
|
||||
(min(progressFirstThumb, progressSecondThumb) - CGFloat(sliderModel.minValue)) * pointWidth
|
||||
let progressSecondThumbWidth =
|
||||
(max(progressFirstThumb, progressSecondThumb) - CGFloat(sliderModel.minValue)) * pointWidth
|
||||
let (originX, progressWidth) = {
|
||||
switch sliderModel.layoutDirection {
|
||||
case .leftToRight:
|
||||
$0.origin
|
||||
.x += (min(progressFirstThumb, progressSecondThumb) - CGFloat(sliderModel.minValue)) *
|
||||
pointWidth
|
||||
$0.size.width = abs(progressFirstThumb - progressSecondThumb) * pointWidth
|
||||
return (progressFirstThumbWidth, progressSecondThumbWidth)
|
||||
case .rightToLeft:
|
||||
$0.origin
|
||||
.x -= (max(progressFirstThumb, progressSecondThumb) - CGFloat(sliderModel.maxValue)) *
|
||||
pointWidth
|
||||
$0.size.width = abs(progressFirstThumb - progressSecondThumb) * pointWidth
|
||||
return (
|
||||
inactiveTrackView.frame.width - progressSecondThumbWidth,
|
||||
inactiveTrackView.frame.width - progressFirstThumbWidth
|
||||
)
|
||||
@unknown default:
|
||||
assertionFailure("Unknown layoutDirection (UserInterfaceLayoutDirection)")
|
||||
return (0, 0)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
activeTrackView.cutView(originX: originX)
|
||||
|
||||
activeTrackView.frame = CGRect(
|
||||
x: inactiveTrackView.frame.minX,
|
||||
y: inactiveTrackView.frame.minY,
|
||||
width: progressWidth,
|
||||
height: sliderModel.sliderHeight
|
||||
)
|
||||
|
||||
marksView.firstThumbProgress = firstThumbProgress
|
||||
marksView.secondThumbProgress = secondThumbProgress
|
||||
@@ -431,3 +450,21 @@ private enum ThumbPosition {
|
||||
}
|
||||
|
||||
private let animationDuration = 0.3
|
||||
|
||||
extension UIView {
|
||||
fileprivate func cutView(originX: CGFloat) {
|
||||
let shapeLayer = CAShapeLayer()
|
||||
let path = CGMutablePath()
|
||||
let rect = CGRect(
|
||||
x: self.bounds.origin.x,
|
||||
y: self.bounds.origin.y,
|
||||
width: originX,
|
||||
height: self.bounds.height
|
||||
)
|
||||
path.addRect(rect)
|
||||
path.addRect(bounds)
|
||||
shapeLayer.path = path
|
||||
shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
||||
layer.mask = shapeLayer
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -131,7 +131,10 @@
|
||||
},
|
||||
"minItems": 1,
|
||||
"$description": "translations.json#/div_slider_ranges",
|
||||
"platforms": []
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
]
|
||||
},
|
||||
"secondary_value_accessibility": {
|
||||
"$ref": "div-accessibility.json",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"description": "Slider with ranges",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"div_data": {
|
||||
"templates": {},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"description": "Slider with ranges",
|
||||
"platforms": [
|
||||
"android"
|
||||
"android",
|
||||
"ios"
|
||||
],
|
||||
"templates": {
|
||||
"test_slider": {
|
||||
|
||||