From c911d562d91bc320bc8ed0eea604b65539340f9a Mon Sep 17 00:00:00 2001 From: wwwcg Date: Tue, 13 Aug 2024 21:29:24 +0800 Subject: [PATCH] fix(ios): resolve floating point size comparison issues (#3989) --- .../native/ios/renderer/HippyUIManager.mm | 2 +- .../component/scrollview/HippyScrollView.mm | 3 +- .../component/textinput/HippyTextView.mm | 4 +- .../component/view/HippyShadowView.mm | 3 +- .../ios/renderer/component/view/HippyView.m | 5 +- .../component/viewPager/HippyViewPager.mm | 8 +- .../waterfalllist/HippyShadowWaterfallItem.m | 3 +- renderer/native/ios/utils/HippyRenderUtils.h | 11 +- renderer/native/ios/utils/HippyRenderUtils.m | 51 ++++- tests/ios/HippyRenderUtilsTest.m | 175 ++++++++++++++++++ 10 files changed, 244 insertions(+), 21 deletions(-) create mode 100644 tests/ios/HippyRenderUtilsTest.m diff --git a/renderer/native/ios/renderer/HippyUIManager.mm b/renderer/native/ios/renderer/HippyUIManager.mm index c063ad487ad..2891ddea9ab 100644 --- a/renderer/native/ios/renderer/HippyUIManager.mm +++ b/renderer/native/ios/renderer/HippyUIManager.mm @@ -426,7 +426,7 @@ - (void)setFrame:(CGRect)frame forView:(UIView *)view{ return; } - if (!CGRectEqualToRect(frame, renderObject.frame)) { + if (!HippyCGRectRoundInPixelNearlyEqual(frame, renderObject.frame)) { //renderObject.frame = frame; [renderObject setLayoutFrame:frame]; std::weak_ptr rootNode = [strongSelf->_shadowViewRegistry rootNodeForTag:rootTag]; diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm index ecc2017025c..4c526add9c5 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm @@ -25,6 +25,7 @@ #import "UIView+Hippy.h" #import "UIView+MountEvent.h" #import "UIView+DirectionalLayout.h" +#import "HippyRenderUtils.h" @implementation HippyCustomScrollView @@ -661,7 +662,7 @@ - (CGSize)contentSize { - (void)hippyBridgeDidFinishTransaction { CGSize contentSize = self.contentSize; - if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) { + if (!HippyCGSizeRoundInPixelNearlyEqual(_scrollView.contentSize, contentSize)) { // When contentSize is set manually, ScrollView internals will reset // contentOffset to {0, 0}. Since we potentially set contentSize whenever // anything in the ScrollView updates, we workaround this issue by manually diff --git a/renderer/native/ios/renderer/component/textinput/HippyTextView.mm b/renderer/native/ios/renderer/component/textinput/HippyTextView.mm index ac1ac05cb92..95c6fbdff9f 100644 --- a/renderer/native/ios/renderer/component/textinput/HippyTextView.mm +++ b/renderer/native/ios/renderer/component/textinput/HippyTextView.mm @@ -27,6 +27,7 @@ #import "HippyUtils.h" #import "HippyTextSelection.h" #import "UIView+Hippy.h" +#import "HippyRenderUtils.h" @implementation HippyUITextView @@ -290,7 +291,8 @@ - (void)updateContentSize { CGSize contentSize = (CGSize) { CGRectGetMaxX(_scrollView.frame), INFINITY }; contentSize.height = [_textView sizeThatFits:contentSize].height; - if (_viewDidCompleteInitialLayout && _onContentSizeChange && !CGSizeEqualToSize(_previousContentSize, contentSize)) { + if (_viewDidCompleteInitialLayout && _onContentSizeChange + && !HippyCGSizeRoundInPixelNearlyEqual(_previousContentSize, contentSize)) { _previousContentSize = contentSize; _onContentSizeChange(@{ @"contentSize": @ { diff --git a/renderer/native/ios/renderer/component/view/HippyShadowView.mm b/renderer/native/ios/renderer/component/view/HippyShadowView.mm index 759988a8e04..17875abdf35 100644 --- a/renderer/native/ios/renderer/component/view/HippyShadowView.mm +++ b/renderer/native/ios/renderer/component/view/HippyShadowView.mm @@ -28,6 +28,7 @@ #import "UIView+Hippy.h" #import "HippyShadowView+Internal.h" #import "HippyAssert.h" +#import "HippyRenderUtils.h" static NSString *const HippyBackgroundColorPropKey = @"backgroundColor"; @@ -286,7 +287,7 @@ - (void)setLayoutFrame:(CGRect)frame { - (void)setLayoutFrame:(CGRect)frame dirtyPropagation:(BOOL)dirtyPropagation { CGRect currentFrame = self.frame; - if (CGRectEqualToRect(currentFrame, frame)) { + if (HippyCGRectRoundInPixelNearlyEqual(currentFrame, frame)) { return; } [self setFrame:frame]; diff --git a/renderer/native/ios/renderer/component/view/HippyView.m b/renderer/native/ios/renderer/component/view/HippyView.m index d577002c0ba..77e0b33d068 100644 --- a/renderer/native/ios/renderer/component/view/HippyView.m +++ b/renderer/native/ios/renderer/component/view/HippyView.m @@ -27,6 +27,7 @@ #import "HippyView.h" #import "UIView+DomEvent.h" #import "UIView+Hippy.h" +#import "HippyRenderUtils.h" static CGSize makeSizeConstrainWithType(CGSize originSize, CGSize constrainSize, NSString *resizeMode) { // width / height @@ -169,7 +170,7 @@ - (void)hippySetFrame:(CGRect)frame { // TODO: detect up-front if re-rendering is necessary CGSize oldSize = self.bounds.size; [super hippySetFrame:frame]; - if (!CGSizeEqualToSize(self.bounds.size, oldSize)) { + if (!HippyCGSizeNearlyEqual(self.bounds.size, oldSize)) { [self.layer setNeedsDisplay]; } } @@ -235,7 +236,7 @@ - (CALayerContentsFilter)magnificationFilter { } - (void)displayLayer:(CALayer *)layer { - if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) { + if (HippyCGSizeNearlyEqual(layer.bounds.size, CGSizeZero)) { return; } diff --git a/renderer/native/ios/renderer/component/viewPager/HippyViewPager.mm b/renderer/native/ios/renderer/component/viewPager/HippyViewPager.mm index e190e2b2e95..68ad2dee578 100644 --- a/renderer/native/ios/renderer/component/viewPager/HippyViewPager.mm +++ b/renderer/native/ios/renderer/component/viewPager/HippyViewPager.mm @@ -26,8 +26,8 @@ #import "UIView+DirectionalLayout.h" #import "UIView+MountEvent.h" #import "HippyLog.h" +#import "HippyRenderUtils.h" -#include "float.h" @interface HippyViewPager () @property (nonatomic, strong) NSMutableArray *viewPagerItems; @@ -435,8 +435,8 @@ - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { } - (void)hippyBridgeDidFinishTransaction { - BOOL isFrameEqual = CGRectEqualToRect(self.frame, self.previousFrame); - BOOL isContentSizeEqual = CGSizeEqualToSize(self.contentSize, self.previousSize); + BOOL isFrameEqual = HippyCGRectRoundInPixelNearlyEqual(self.frame, self.previousFrame); + BOOL isContentSizeEqual = HippyCGSizeRoundInPixelNearlyEqual(self.contentSize, self.previousSize); if (!isContentSizeEqual || !isFrameEqual) { self.previousFrame = self.frame; self.previousSize = self.contentSize; @@ -474,7 +474,7 @@ - (void)layoutSubviews { CGSize updatedSize = CGSizeMake(lastViewPagerItem.frame.origin.x + lastViewPagerItem.frame.size.width, lastViewPagerItem.frame.origin.y + lastViewPagerItem.frame.size.height); - if (!CGSizeEqualToSize(self.contentSize, updatedSize)) { + if (!HippyCGSizeRoundInPixelNearlyEqual(self.contentSize, updatedSize)) { self.contentSize = updatedSize; } diff --git a/renderer/native/ios/renderer/component/waterfalllist/HippyShadowWaterfallItem.m b/renderer/native/ios/renderer/component/waterfalllist/HippyShadowWaterfallItem.m index 167412d8707..b5271f0000f 100644 --- a/renderer/native/ios/renderer/component/waterfalllist/HippyShadowWaterfallItem.m +++ b/renderer/native/ios/renderer/component/waterfalllist/HippyShadowWaterfallItem.m @@ -21,6 +21,7 @@ */ #import "HippyShadowWaterfallItem.h" +#import "HippyRenderUtils.h" @implementation HippyShadowWaterfallItem @@ -35,7 +36,7 @@ - (instancetype)init { - (void)setFrame:(CGRect)frame { CGRect originFrame = self.frame; [super setFrame:frame]; - if (!CGSizeEqualToSize(originFrame.size, frame.size) && + if (!HippyCGSizeRoundInPixelNearlyEqual(originFrame.size, frame.size) && [self.observer respondsToSelector:@selector(itemFrameChanged:)]) { [self.observer itemFrameChanged:self]; } diff --git a/renderer/native/ios/utils/HippyRenderUtils.h b/renderer/native/ios/utils/HippyRenderUtils.h index 99a544eb3b8..651802ba505 100644 --- a/renderer/native/ios/utils/HippyRenderUtils.h +++ b/renderer/native/ios/utils/HippyRenderUtils.h @@ -34,11 +34,18 @@ HIPPY_EXTERN CGFloat HippyRoundPixelValue(CGFloat value); HIPPY_EXTERN CGFloat HippyCeilPixelValue(CGFloat value); HIPPY_EXTERN CGFloat HippyFloorPixelValue(CGFloat value); -// Convert a size in points to pixels, rounded up to the nearest integral size -HIPPY_EXTERN CGSize HippySizeInPixels(CGSize pointSize, CGFloat scale); +/// Convert a size in points to pixels, rounded up to the nearest integral size +FOUNDATION_EXTERN CGSize HippySizeCeilInPixels(CGSize pointSize, CGFloat scale); +/// Convert a size in points to pixels, rounded to the nearest integral size +FOUNDATION_EXTERN CGSize HippySizeRoundInPixels(CGSize pointSize, CGFloat scale); HIPPY_EXTERN BOOL HippyCGRectNearlyEqual(CGRect frame1, CGRect frame2); HIPPY_EXTERN BOOL HippyCGPointNearlyEqual(CGPoint point1, CGPoint point2); HIPPY_EXTERN BOOL HippyCGSizeNearlyEqual(CGSize size1, CGSize size2); +/// First convert size in points to pixels by HippySizeRoundInPixels, then compare. +HIPPY_EXTERN BOOL HippyCGSizeRoundInPixelNearlyEqual(CGSize size1, CGSize size2); +HIPPY_EXTERN BOOL HippyCGRectRoundInPixelNearlyEqual(CGRect frame1, CGRect frame2); +HIPPY_EXTERN BOOL HippyCGPointRoundInPixelNearlyEqual(CGPoint point1, CGPoint point2); + NS_ASSUME_NONNULL_END diff --git a/renderer/native/ios/utils/HippyRenderUtils.m b/renderer/native/ios/utils/HippyRenderUtils.m index 29252b937ec..475244ac255 100644 --- a/renderer/native/ios/utils/HippyRenderUtils.m +++ b/renderer/native/ios/utils/HippyRenderUtils.m @@ -23,13 +23,15 @@ #import "HippyUtils.h" #import "HippyRenderUtils.h" +// Use global variable to facilitate unit test +CGFloat gHippyScreenScaleValue = CGFLOAT_MAX; + CGFloat HippyScreenScale(void) { - static CGFloat scale = CGFLOAT_MAX; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - scale = [UIScreen mainScreen].scale; + gHippyScreenScaleValue = [UIScreen mainScreen].scale; }); - return scale; + return gHippyScreenScaleValue; } CGSize HippyScreenSize(void) { @@ -56,24 +58,57 @@ CGFloat HippyFloorPixelValue(CGFloat value) { return floor(value * scale) / scale; } -CGSize HippySizeInPixels(CGSize pointSize, CGFloat scale) { +CGSize HippySizeCeilInPixels(CGSize pointSize, CGFloat scale) { return (CGSize) { ceil(pointSize.width * scale), ceil(pointSize.height * scale), }; } +CGSize HippySizeRoundInPixels(CGSize pointSize, CGFloat scale) { + return (CGSize) { + round(pointSize.width * scale), + round(pointSize.height * scale), + }; +} + BOOL HippyCGRectNearlyEqual(CGRect frame1, CGRect frame2) { return HippyCGPointNearlyEqual(frame1.origin, frame2.origin) && HippyCGSizeNearlyEqual(frame1.size, frame2.size); } BOOL HippyCGPointNearlyEqual(CGPoint point1, CGPoint point2) { - return fabs(point1.x - point2.x) < CGFLOAT_EPSILON && - fabs(point1.y - point2.y) < CGFLOAT_EPSILON; + return fabs(point1.x - point2.x) < 3 * CGFLOAT_EPSILON && + fabs(point1.y - point2.y) < 3 * CGFLOAT_EPSILON; } BOOL HippyCGSizeNearlyEqual(CGSize size1, CGSize size2) { - return fabs(size1.width - size2.width) < CGFLOAT_EPSILON && - fabs(size1.height - size2.height) < CGFLOAT_EPSILON; + return fabs(size1.width - size2.width) < 3 * CGFLOAT_EPSILON && + fabs(size1.height - size2.height) < 3 * CGFLOAT_EPSILON; +} + +BOOL HippyCGSizeRoundInPixelNearlyEqual(CGSize size1, CGSize size2) { + CGFloat scale = HippyScreenScale(); + CGSize sizeA = HippySizeRoundInPixels(size1, scale); + CGSize sizeB = HippySizeRoundInPixels(size2, scale); + return HippyCGSizeNearlyEqual(sizeA,sizeB); +} + +BOOL HippyCGPointRoundInPixelNearlyEqual(CGPoint point1, CGPoint point2) { + CGFloat scale = HippyScreenScale(); + CGPoint pointA = (CGPoint) { + round(point1.x * scale), + round(point1.y * scale), + }; + CGPoint pointB = (CGPoint) { + round(point2.x * scale), + round(point2.y * scale), + }; + return fabs(pointA.x - pointB.x) < CGFLOAT_EPSILON && + fabs(pointA.y - pointB.y) < CGFLOAT_EPSILON; +} + +BOOL HippyCGRectRoundInPixelNearlyEqual(CGRect frame1, CGRect frame2) { + return HippyCGPointRoundInPixelNearlyEqual(frame1.origin, frame2.origin) && + HippyCGSizeRoundInPixelNearlyEqual(frame1.size, frame2.size); } diff --git a/tests/ios/HippyRenderUtilsTest.m b/tests/ios/HippyRenderUtilsTest.m new file mode 100644 index 00000000000..38d587a83dd --- /dev/null +++ b/tests/ios/HippyRenderUtilsTest.m @@ -0,0 +1,175 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import + + +@interface HippyRenderUtilsTest : XCTestCase + +@end + +@implementation HippyRenderUtilsTest + +// defined in HippyRenderUtils +extern CGFloat gHippyScreenScaleValue; + +- (void)setUp { + // assume that the screen scale is 2.0. + gHippyScreenScaleValue = 3.0; +} + +- (void)tearDown { + // nop +} + +- (void)testHippyCGSizeCompare { + CGSize size1 = CGSizeMake(10.5, 20.5); + CGSize size2 = CGSizeMake(10.5, 20.5); + XCTAssertTrue(HippyCGSizeNearlyEqual(size1, size2)); + BOOL result = HippyCGSizeRoundInPixelNearlyEqual(size1, size2); + XCTAssertTrue(result, @"Sizes should be nearly equal"); + + // assume that the screen scale is 2.0. + gHippyScreenScaleValue = 2.0; + CGSize size3 = CGSizeMake(10.0, 20.5); + CGSize size4 = CGSizeMake(10.0, 21.0); + XCTAssertFalse(HippyCGSizeNearlyEqual(size3, size4)); + result = HippyCGSizeRoundInPixelNearlyEqual(size3, size4); + XCTAssertFalse(result, @"Sizes should not be nearly equal"); + + // assume that the screen scale is 3.0. + gHippyScreenScaleValue = 3.0; + size3 = CGSizeMake(10.333, 20.5); + size4 = CGSizeMake(10.666, 20.5); + XCTAssertFalse(HippyCGSizeNearlyEqual(size3, size4)); + result = HippyCGSizeRoundInPixelNearlyEqual(size3, size4); + XCTAssertFalse(result, @"Sizes should not be nearly equal"); + + CGSize size5 = CGSizeMake(1.3333356, 1.3333356); + CGSize size6 = CGSizeMake(1.3333289, 1.3333289); + XCTAssertFalse(CGSizeEqualToSize(size5, size6)); + XCTAssertFalse(HippyCGSizeNearlyEqual(size5, size6)); + result = HippyCGSizeRoundInPixelNearlyEqual(size5, size6); + XCTAssertTrue(result, @"Sizes should be nearly equal in edge case"); +} + +- (void)testHippyCGPointCompare { + CGPoint point1 = CGPointMake(10.5, 20.5); + CGPoint point2 = CGPointMake(10.5, 20.5); + XCTAssertTrue(HippyCGPointNearlyEqual(point1, point2)); + BOOL result = HippyCGPointRoundInPixelNearlyEqual(point1, point2); + XCTAssertTrue(result, @"Points should be nearly equal"); + + CGPoint point3 = CGPointMake(10.3, 20.5); + CGPoint point4 = CGPointMake(10.6, 20.5); + XCTAssertFalse(HippyCGPointNearlyEqual(point3, point4)); + result = HippyCGPointRoundInPixelNearlyEqual(point3, point4); + XCTAssertFalse(result, @"Points should not be nearly equal"); + + CGPoint point5 = CGPointMake(1.3333356, 1.3333356); + CGPoint point6 = CGPointMake(1.3333289, 1.3333289); + XCTAssertFalse(CGPointEqualToPoint(point5, point6)); + XCTAssertFalse(HippyCGPointNearlyEqual(point5, point6)); + result = HippyCGPointRoundInPixelNearlyEqual(point5, point6); + XCTAssertTrue(result, @"Points should be nearly equal in edge case"); +} + +- (void)testHippyCGRectCompare { + CGRect rect1 = CGRectMake(10.5, 20.5, 30.5, 40.5); + CGRect rect2 = CGRectMake(10.5, 20.5, 30.5, 40.5); + XCTAssertTrue(HippyCGRectNearlyEqual(rect1, rect2)); + BOOL result = HippyCGRectRoundInPixelNearlyEqual(rect1, rect2); + XCTAssertTrue(result, @"Rects should be nearly equal"); + + CGRect rect3 = CGRectMake(10.3, 20.5, 30.5, 40.5); + CGRect rect4 = CGRectMake(10.6, 20.5, 30.5, 40.5); + XCTAssertFalse(HippyCGRectNearlyEqual(rect3, rect4)); + result = HippyCGRectRoundInPixelNearlyEqual(rect3, rect4); + XCTAssertFalse(result, @"Rects should not be nearly equal"); + + CGRect rect5 = CGRectMake(1.3333356, 1.3333356, 1.3333356, 1.3333356); + CGRect rect6 = CGRectMake(1.3333289, 1.3333289, 1.3333289, 1.3333289); + XCTAssertFalse(CGRectEqualToRect(rect5, rect6)); + XCTAssertFalse(HippyCGRectNearlyEqual(rect5, rect6)); + result = HippyCGRectRoundInPixelNearlyEqual(rect5, rect6); + XCTAssertTrue(result, @"Rects should be nearly equal in edge case"); +} + +- (void)testHippyRoundPixelValue { + CGFloat value1 = 1.3333356; + CGFloat expected1 = round(value1 * HippyScreenScale()) / HippyScreenScale(); + CGFloat result1 = HippyRoundPixelValue(value1); + XCTAssertEqual(result1, expected1, @"Rounded pixel value should be equal"); + + CGFloat value2 = 1.3333289; + CGFloat expected2 = round(value2 * HippyScreenScale()) / HippyScreenScale(); + CGFloat result2 = HippyRoundPixelValue(value2); + XCTAssertEqual(result2, expected2, @"Rounded pixel value should be equal"); +} + +- (void)testHippyCeilPixelValue { + CGFloat value1 = 1.3333356; + CGFloat expected1 = ceil(value1 * HippyScreenScale()) / HippyScreenScale(); + CGFloat result1 = HippyCeilPixelValue(value1); + XCTAssertEqual(result1, expected1, @"Ceil pixel value should be equal"); + + CGFloat value2 = 1.3333289; + CGFloat expected2 = ceil(value2 * HippyScreenScale()) / HippyScreenScale(); + CGFloat result2 = HippyCeilPixelValue(value2); + XCTAssertEqual(result2, expected2, @"Ceil pixel value should be equal"); +} + +- (void)testHippyFloorPixelValue { + CGFloat value1 = 1.3333356; + CGFloat expected1 = floor(value1 * HippyScreenScale()) / HippyScreenScale(); + CGFloat result1 = HippyFloorPixelValue(value1); + XCTAssertEqual(result1, expected1, @"Floor pixel value should be equal"); + + CGFloat value2 = 1.3333289; + CGFloat expected2 = floor(value2 * HippyScreenScale()) / HippyScreenScale(); + CGFloat result2 = HippyFloorPixelValue(value2); + XCTAssertEqual(result2, expected2, @"Floor pixel value should be equal"); +} + +- (void)testHippySizeCeilInPixels { + CGSize size1 = CGSizeMake(1.3333356, 1.3333356); + CGFloat scale = HippyScreenScale(); + CGSize expected1 = (CGSize){ + ceil(size1.width * scale), + ceil(size1.height * scale), + }; + CGSize result1 = HippySizeCeilInPixels(size1, scale); + XCTAssertTrue(CGSizeEqualToSize(result1, expected1), @"Ceil size in pixels should be equal"); + + CGSize size2 = CGSizeMake(1.3333289, 1.3333289); + CGSize expected2 = (CGSize){ + ceil(size2.width * scale), + ceil(size2.height * scale), + }; + CGSize result2 = HippySizeCeilInPixels(size2, scale); + XCTAssertTrue(CGSizeEqualToSize(result2, expected2), @"Ceil size in pixels should be equal"); +} + + +@end