fix: 固定首页常用应用图标尺寸
This commit is contained in:
@ -8,14 +8,49 @@ import UIKit
|
|||||||
/// 首页 / 全部功能菜单图标加载,统一 SF Symbol 与资源图的视觉尺寸。
|
/// 首页 / 全部功能菜单图标加载,统一 SF Symbol 与资源图的视觉尺寸。
|
||||||
enum HomeMenuIconFactory {
|
enum HomeMenuIconFactory {
|
||||||
|
|
||||||
|
/// 菜单图标统一使用的逻辑画布尺寸。
|
||||||
|
static let canvasSize = CGSize(width: 24, height: 24)
|
||||||
|
|
||||||
/// 与菜单图标槽位(约 24–26pt)对齐的系统图标配置。
|
/// 与菜单图标槽位(约 24–26pt)对齐的系统图标配置。
|
||||||
static let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
|
static let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
|
||||||
|
|
||||||
/// 优先加载 Assets 资源图;否则按统一配置加载 SF Symbol。
|
private static let imageCache = NSCache<NSString, UIImage>()
|
||||||
|
|
||||||
|
/// 优先加载 Assets 资源图;否则加载 SF Symbol,并统一渲染到固定画布。
|
||||||
static func image(named iconName: String) -> UIImage? {
|
static func image(named iconName: String) -> UIImage? {
|
||||||
if let asset = UIImage(named: iconName) {
|
let cacheKey = iconName as NSString
|
||||||
return asset
|
if let cachedImage = imageCache.object(forKey: cacheKey) {
|
||||||
|
return cachedImage
|
||||||
}
|
}
|
||||||
return UIImage(systemName: iconName, withConfiguration: symbolConfiguration)
|
|
||||||
|
guard let sourceImage = UIImage(named: iconName)
|
||||||
|
?? UIImage(systemName: iconName, withConfiguration: symbolConfiguration) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedImage = renderOnFixedCanvas(sourceImage)
|
||||||
|
imageCache.setObject(normalizedImage, forKey: cacheKey)
|
||||||
|
return normalizedImage
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func renderOnFixedCanvas(_ image: UIImage) -> UIImage {
|
||||||
|
let sourceSize = image.size
|
||||||
|
guard sourceSize.width > 0, sourceSize.height > 0 else {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = min(canvasSize.width / sourceSize.width, canvasSize.height / sourceSize.height)
|
||||||
|
let drawSize = CGSize(width: sourceSize.width * scale, height: sourceSize.height * scale)
|
||||||
|
let drawRect = CGRect(
|
||||||
|
x: (canvasSize.width - drawSize.width) / 2,
|
||||||
|
y: (canvasSize.height - drawSize.height) / 2,
|
||||||
|
width: drawSize.width,
|
||||||
|
height: drawSize.height
|
||||||
|
)
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: canvasSize)
|
||||||
|
let renderedImage = renderer.image { _ in
|
||||||
|
image.withRenderingMode(.alwaysOriginal).draw(in: drawRect)
|
||||||
|
}
|
||||||
|
return renderedImage.withRenderingMode(.alwaysTemplate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,9 +75,8 @@ final class AllFunctionMenuCell: UICollectionViewCell {
|
|||||||
cardView.clipsToBounds = true
|
cardView.clipsToBounds = true
|
||||||
|
|
||||||
iconView.tintColor = AppColor.primary
|
iconView.tintColor = AppColor.primary
|
||||||
iconView.contentMode = .scaleAspectFit
|
iconView.contentMode = .center
|
||||||
iconView.preferredSymbolConfiguration = HomeMenuIconFactory.symbolConfiguration
|
iconView.adjustsImageSizeForAccessibilityContentSizeCategory = false
|
||||||
|
|
||||||
|
|
||||||
contentStackView.axis = .vertical
|
contentStackView.axis = .vertical
|
||||||
contentStackView.alignment = .center
|
contentStackView.alignment = .center
|
||||||
|
|||||||
@ -127,6 +127,13 @@ final class HomeMenuCell: UICollectionViewCell {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
iconView.image = nil
|
||||||
|
iconView.accessibilityIdentifier = nil
|
||||||
|
titleLabel.text = nil
|
||||||
|
}
|
||||||
|
|
||||||
func apply(menu: HomeMenuItem) {
|
func apply(menu: HomeMenuItem) {
|
||||||
iconView.image = HomeMenuIconFactory.image(named: menu.iconName)
|
iconView.image = HomeMenuIconFactory.image(named: menu.iconName)
|
||||||
iconView.accessibilityIdentifier = menu.iconName
|
iconView.accessibilityIdentifier = menu.iconName
|
||||||
@ -146,9 +153,8 @@ final class HomeMenuCell: UICollectionViewCell {
|
|||||||
iconContainerView.layer.cornerRadius = 20
|
iconContainerView.layer.cornerRadius = 20
|
||||||
|
|
||||||
iconView.tintColor = AppColor.primary
|
iconView.tintColor = AppColor.primary
|
||||||
iconView.contentMode = .scaleAspectFit
|
iconView.contentMode = .center
|
||||||
iconView.preferredSymbolConfiguration = HomeMenuIconFactory.symbolConfiguration
|
iconView.adjustsImageSizeForAccessibilityContentSizeCategory = false
|
||||||
|
|
||||||
|
|
||||||
contentStackView.axis = .vertical
|
contentStackView.axis = .vertical
|
||||||
contentStackView.alignment = .center
|
contentStackView.alignment = .center
|
||||||
@ -176,6 +182,8 @@ final class HomeMenuCell: UICollectionViewCell {
|
|||||||
iconContainerView.snp.makeConstraints { make in
|
iconContainerView.snp.makeConstraints { make in
|
||||||
make.width.height.equalTo(40)
|
make.width.height.equalTo(40)
|
||||||
}
|
}
|
||||||
|
iconContainerView.setContentHuggingPriority(.required, for: .vertical)
|
||||||
|
iconContainerView.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
iconView.snp.makeConstraints { make in
|
iconView.snp.makeConstraints { make in
|
||||||
make.center.equalToSuperview()
|
make.center.equalToSuperview()
|
||||||
make.width.height.equalTo(24)
|
make.width.height.equalTo(24)
|
||||||
|
|||||||
@ -14,27 +14,25 @@ final class HomeMenuIconFactoryTests: XCTestCase {
|
|||||||
let image = HomeMenuIconFactory.image(named: "home_menu_space")
|
let image = HomeMenuIconFactory.image(named: "home_menu_space")
|
||||||
|
|
||||||
XCTAssertNotNil(image)
|
XCTAssertNotNil(image)
|
||||||
|
XCTAssertEqual(image?.size, HomeMenuIconFactory.canvasSize)
|
||||||
XCTAssertNil(UIImage(systemName: "home_menu_space"))
|
XCTAssertNil(UIImage(systemName: "home_menu_space"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSystemSymbolMatchesSharedConfiguration() {
|
func testSystemSymbolsUseFixedCanvas() {
|
||||||
let expected = UIImage(
|
let iconNames = [
|
||||||
systemName: "ellipsis",
|
"ellipsis",
|
||||||
withConfiguration: HomeMenuIconFactory.symbolConfiguration
|
"dot.radiowaves.left.and.right",
|
||||||
)
|
"rectangle.stack.badge.plus",
|
||||||
let actual = HomeMenuIconFactory.image(named: "ellipsis")
|
]
|
||||||
|
|
||||||
XCTAssertNotNil(actual)
|
for iconName in iconNames {
|
||||||
XCTAssertEqual(actual?.size, expected?.size)
|
let image = HomeMenuIconFactory.image(named: iconName)
|
||||||
}
|
|
||||||
|
|
||||||
func testSystemSymbolDiffersFromUnconfiguredDefault() {
|
XCTAssertNotNil(image)
|
||||||
let configured = HomeMenuIconFactory.image(named: "ellipsis")
|
XCTAssertEqual(image?.size, HomeMenuIconFactory.canvasSize)
|
||||||
let plain = UIImage(systemName: "ellipsis")
|
XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
|
||||||
|
XCTAssertFalse(image?.isSymbolImage ?? true)
|
||||||
XCTAssertNotNil(configured)
|
}
|
||||||
XCTAssertNotNil(plain)
|
|
||||||
XCTAssertNotEqual(configured?.size, plain?.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComplexSystemSymbolResolves() {
|
func testComplexSystemSymbolResolves() {
|
||||||
@ -42,3 +40,41 @@ final class HomeMenuIconFactoryTests: XCTestCase {
|
|||||||
XCTAssertNotNil(HomeMenuIconFactory.image(named: "rectangle.stack.badge.plus"))
|
XCTAssertNotNil(HomeMenuIconFactory.image(named: "rectangle.stack.badge.plus"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 首页常用应用 cell 的图标布局与复用测试。
|
||||||
|
@MainActor
|
||||||
|
final class HomeMenuCellTests: XCTestCase {
|
||||||
|
|
||||||
|
func testIconKeepsFixedSizeWhenReusedAtIPhoneXWidth() throws {
|
||||||
|
let cell = HomeMenuCell(frame: CGRect(x: 0, y: 0, width: 109, height: 109))
|
||||||
|
let menus = [
|
||||||
|
HomeMenuItem(uri: "space", title: "空间设置", iconName: "home_menu_space"),
|
||||||
|
HomeMenuItem(uri: "live", title: "直播管理", iconName: "dot.radiowaves.left.and.right"),
|
||||||
|
HomeMenuItem(uri: "more", title: "更多功能", iconName: "ellipsis"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for menu in menus {
|
||||||
|
cell.prepareForReuse()
|
||||||
|
cell.apply(menu: menu)
|
||||||
|
cell.setNeedsLayout()
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
|
||||||
|
let iconView = try XCTUnwrap(findImageView(in: cell, identifier: menu.iconName))
|
||||||
|
XCTAssertEqual(iconView.bounds.size, CGSize(width: 24, height: 24))
|
||||||
|
XCTAssertEqual(iconView.superview?.bounds.size, CGSize(width: 40, height: 40))
|
||||||
|
XCTAssertEqual(iconView.image?.size, HomeMenuIconFactory.canvasSize)
|
||||||
|
XCTAssertEqual(iconView.contentMode, .center)
|
||||||
|
XCTAssertNil(iconView.preferredSymbolConfiguration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findImageView(in view: UIView, identifier: String) -> UIImageView? {
|
||||||
|
if let imageView = view as? UIImageView,
|
||||||
|
imageView.accessibilityIdentifier == identifier {
|
||||||
|
return imageView
|
||||||
|
}
|
||||||
|
return view.subviews.lazy.compactMap {
|
||||||
|
self.findImageView(in: $0, identifier: identifier)
|
||||||
|
}.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user