Skip to content

Commit

Permalink
fix(ios): resolve floating point size comparison issues
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwcg committed Aug 7, 2024
1 parent 22c99ec commit 2c089f9
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 14 deletions.
2 changes: 1 addition & 1 deletion renderer/native/ios/renderer/HippyUIManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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> rootNode = [strongSelf->_shadowViewRegistry rootNodeForTag:rootTag];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#import "UIView+Hippy.h"
#import "UIView+MountEvent.h"
#import "UIView+DirectionalLayout.h"
#import "HippyRenderUtils.h"

@implementation HippyCustomScrollView

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#import "HippyUtils.h"
#import "HippyTextSelection.h"
#import "UIView+Hippy.h"
#import "HippyRenderUtils.h"

@implementation HippyUITextView

Expand Down Expand Up @@ -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": @ {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#import "UIView+Hippy.h"
#import "HippyShadowView+Internal.h"
#import "HippyAssert.h"
#import "HippyRenderUtils.h"


static NSString *const HippyBackgroundColorPropKey = @"backgroundColor";
Expand Down Expand Up @@ -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];
Expand Down
5 changes: 3 additions & 2 deletions renderer/native/ios/renderer/component/view/HippyView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (!HippyCGSizeRoundInPixelNearlyEqual(self.bounds.size, oldSize)) {
[self.layer setNeedsDisplay];
}
}
Expand Down Expand Up @@ -235,7 +236,7 @@ - (CALayerContentsFilter)magnificationFilter {
}

- (void)displayLayer:(CALayer *)layer {
if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
if (HippyCGSizeNearlyEqual(layer.bounds.size, CGSizeZero)) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIView *> *viewPagerItems;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

#import "HippyShadowWaterfallItem.h"
#import "HippyRenderUtils.h"

@implementation HippyShadowWaterfallItem

Expand All @@ -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];
}
Expand Down
11 changes: 9 additions & 2 deletions renderer/native/ios/utils/HippyRenderUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 34 additions & 1 deletion renderer/native/ios/utils/HippyRenderUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,20 @@ 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);
Expand All @@ -77,3 +84,29 @@ BOOL HippyCGSizeNearlyEqual(CGSize size1, CGSize size2) {
return fabs(size1.width - size2.width) < CGFLOAT_EPSILON &&
fabs(size1.height - size2.height) < 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);
}
171 changes: 171 additions & 0 deletions tests/ios/HippyRenderUtilsTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*!
* 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 <XCTest/XCTest.h>
#import <UIKit/UIKit.h>
#import <OCMock/OCMock.h>
#import <hippy/HippyRenderUtils.h>

@interface HippyRenderUtilsTest : XCTestCase

/// UIScreen's mock object
@property (nonatomic, strong) id screenMock;

@end

@implementation HippyRenderUtilsTest

- (void)setUp {
// create a mock object to mock UIScreen
self.screenMock = OCMClassMock([UIScreen class]);
OCMStub([self.screenMock mainScreen]).andReturn(self.screenMock);
// assume that the screen scale is 3.0.
OCMStub([(UIScreen *)self.screenMock scale]).andReturn(3.0);
}

- (void)tearDown {
// stop mocking
[self.screenMock stopMocking];
self.screenMock = nil;
[super tearDown];
}

- (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");

CGSize size3 = CGSizeMake(10.4, 20.5);
CGSize size4 = CGSizeMake(10.5, 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.4, 20.5);
CGPoint point4 = CGPointMake(10.5, 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.4, 20.5, 30.5, 40.5);
CGRect rect4 = CGRectMake(10.5, 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

0 comments on commit 2c089f9

Please sign in to comment.