diff --git a/.mapping.json b/.mapping.json index f066865a5..fc5f1c24f 100644 --- a/.mapping.json +++ b/.mapping.json @@ -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", diff --git a/client/ios/DivKit/Extensions/DivSliderExtensions.swift b/client/ios/DivKit/Extensions/DivSliderExtensions.swift index f911eb766..d7c0d4c33 100644 --- a/client/ios/DivKit/Extensions/DivSliderExtensions.swift +++ b/client/ios/DivKit/Extensions/DivSliderExtensions.swift @@ -19,16 +19,42 @@ extension DivSlider: DivBlockModeling { private func makeBaseBlock(context: DivBlockModelingContext) throws -> Block { let expressionResolver = context.expressionResolver + + let firstThumbValue: Binding = 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 = 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( diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift index b41d4fe9c..98ec57dbe 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/MarksConfiguration.swift @@ -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 + } } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift index 0a3c2aa95..6fd6a4efd 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderModel.swift @@ -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 } } diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift index 5271b7e7d..587c6b509 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Slider/SliderView.swift @@ -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(_ 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 + } +} diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x.png new file mode 100644 index 000000000..e389a3a21 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step0.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step0.png new file mode 100644 index 000000000..e8076ee36 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step0.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step1.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step1.png new file mode 100644 index 000000000..44a029e18 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step1.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step2.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step2.png new file mode 100644 index 000000000..7214a0e02 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step2.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step3.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step3.png new file mode 100644 index 000000000..bce8b2234 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step3.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step4.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step4.png new file mode 100644 index 000000000..f07f72c17 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step4.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step5.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step5.png new file mode 100644 index 000000000..a43734f21 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_375@2x_step5.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x.png new file mode 100644 index 000000000..4fa6f25c5 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step0.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step0.png new file mode 100644 index 000000000..891963417 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step0.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step1.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step1.png new file mode 100644 index 000000000..add3636aa Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step1.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step2.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step2.png new file mode 100644 index 000000000..02681a73c Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step2.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step3.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step3.png new file mode 100644 index 000000000..a65411ae2 Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step3.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step4.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step4.png new file mode 100644 index 000000000..c421affac Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step4.png differ diff --git a/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step5.png b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step5.png new file mode 100644 index 000000000..6b1848abf Binary files /dev/null and b/client/ios/Tests/reference_snapshots/div-slider/ranges_414@3x_step5.png differ diff --git a/schema/div-slider.json b/schema/div-slider.json index c17775470..920176172 100644 --- a/schema/div-slider.json +++ b/schema/div-slider.json @@ -131,7 +131,10 @@ }, "minItems": 1, "$description": "translations.json#/div_slider_ranges", - "platforms": [] + "platforms": [ + "android", + "ios" + ] }, "secondary_value_accessibility": { "$ref": "div-accessibility.json", diff --git a/test_data/interactive_snapshot_test_data/div-slider/ranges.json b/test_data/interactive_snapshot_test_data/div-slider/ranges.json index c11f9257a..726ed07aa 100644 --- a/test_data/interactive_snapshot_test_data/div-slider/ranges.json +++ b/test_data/interactive_snapshot_test_data/div-slider/ranges.json @@ -1,7 +1,8 @@ { "description": "Slider with ranges", "platforms": [ - "android" + "android", + "ios" ], "div_data": { "templates": {}, diff --git a/test_data/snapshot_test_data/div-slider/ranges.json b/test_data/snapshot_test_data/div-slider/ranges.json index bfc6ba467..bf3720934 100644 --- a/test_data/snapshot_test_data/div-slider/ranges.json +++ b/test_data/snapshot_test_data/div-slider/ranges.json @@ -1,7 +1,8 @@ { "description": "Slider with ranges", "platforms": [ - "android" + "android", + "ios" ], "templates": { "test_slider": {