自定义视图绘制与代码重构:打造高效美观的图像视图
在开发过程中,我们常常需要对视图进行自定义绘制,以实现特定的视觉效果。本文将详细介绍如何为图像视图添加背景渐变、光泽效果,并对代码进行重构,以提高代码的可读性和可维护性。
1. 添加背景渐变和基本属性设置
首先,我们需要设置图像视图的基本属性,包括主图像、背景颜色、边框颜色、阴影和背景渐变。以下是相关代码:
imageView.mainImage = [NSImage imageNamed:@"SpaceShuttle"];
imageView.backgroundColor = [NSColor darkGrayColor];
imageView.borderColor = [NSColor whiteColor];
imageView.shouldAddShadow = YES;
imageView.backgroundGradient = gradient;
[gradient release];
// resize with the window.
NSInteger resizingMask = ( NSViewWidthSizable | NSViewHeightSizable );
保存所有文件并重新运行项目,你将看到主图像后面出现背景渐变。
2. 绘制图像光泽效果
绘制图像光泽效果可以模拟玻璃表面材质。关键是要使用从透明度约为 0.8 的白色渐变到透明度为 0.0 的白色的渐变,并使用多停止渐变来优化外观。
2.1 计算光泽路径
创建一个新的 Objective-C 类别
NSBezierPath-Utilities.m
,并修改头文件
NSBezierPath-Utilities.h
如下:
#import <Cocoa/Cocoa.h>
@interface NSBezierPath (Utilities)
+ (NSBezierPath*) sheenPathForRect:(NSRect)myRect;
@end
实现文件
NSBezierPath-Utilities.m
的代码如下:
#import "NSBezierPath-Utilities.h"
@implementation NSBezierPath (Utilities)
+ (NSBezierPath*) sheenPathForRect:(NSRect)myRect {
CGFloat minX = NSMinX(myRect);
CGFloat maxX = NSMaxX(myRect);
CGFloat maxY = NSMaxY(myRect);
// scale the base of the sheen with the image.
CGFloat bottomLeftY = myRect.origin.y + (myRect.size.height * 0.25);
CGFloat bottomRightY = myRect.origin.y + (myRect.size.height * 0.9);
NSPoint point1 = NSMakePoint ( minX, bottomLeftY );
NSPoint point2 = NSMakePoint ( maxX, bottomRightY );
NSPoint point3 = NSMakePoint ( maxX, maxY );
NSPoint point4 = NSMakePoint ( minX, maxY );
// for the arc that crosses the image.
NSPoint control1 = NSMakePoint ( minX * 0.9, maxY * 0.9 );
NSPoint control2 = NSMakePoint ( maxX * 0.9, maxY * 0.9 );
// create the path.
NSBezierPath* sheenPath = [NSBezierPath bezierPath];
// starting point.
[sheenPath moveToPoint: point1];
// arc to the other side.
[sheenPath curveToPoint: point2
controlPoint1: control1
controlPoint2: control2];
[sheenPath lineToPoint:point3];
[sheenPath lineToPoint:point4];
[sheenPath closePath];
return sheenPath;
}
@end
这个类方法创建了一个新的
NSBezierPath
实例,可用于绘制光泽。
2.2 添加光泽属性和方法
在
StyledImageView.h
中添加公共属性以启用光泽:
@interface StyledImageView : NSView
@property (retain) NSImage* mainImage;
@property (retain) NSColor* backgroundColor;
@property (retain) NSColor* borderColor;
@property (assign) BOOL shouldAddShadow;
@property (retain) NSGradient* backgroundGradient;
@property (assign) BOOL shouldAddSheen;
@end
在
StyledImageView.m
中添加私有光泽属性和方法:
@interface StyledImageView ()
// private properties.
@property (retain) NSShadow* imageShadow;
@property (retain) NSGradient* imageSheen;
// private methods.
+ (NSShadow*) defaultImageShadow;
+ (NSGradient*) defaultImageSheen;
- (void) drawBackgroundInRect:(NSRect)rect;
- (void) drawImageSheenInRect:(NSRect)rect;
@end
添加合成语句:
@implementation StyledImageView
@synthesize mainImage;
@synthesize backgroundColor;
@synthesize borderColor;
@synthesize shouldAddShadow;
@synthesize imageShadow;
@synthesize backgroundGradient;
@synthesize shouldAddSheen;
@synthesize imageSheen;
在
dealloc
方法中释放光泽渐变:
- (void) dealloc {
self.mainImage = nil;
self.backgroundColor = nil;
self.borderColor = nil;
self.imageShadow = nil;
self.backgroundGradient = nil;
self.imageSheen = nil;
[super dealloc];
}
重写
setShouldAddSheen:
方法,以便在启用光泽时重新生成渐变:
- (void) setShouldAddSheen:(BOOL)shouldAdd {
// if the new value is the same, just return.
if ( shouldAddSheen == shouldAdd ) return;
// set the new value.
shouldAddSheen = shouldAdd;
if ( shouldAddSheen )
self.imageSheen = [[self class] defaultImageSheen];
else
self.imageSheen = nil;
// redraw.
[self setNeedsDisplay:YES];
}
添加私有类方法以生成光泽渐变:
+ (NSGradient*) defaultImageSheen {
NSColor* color1 = [NSColor colorWithDeviceWhite:1.0 alpha:0.80];
NSColor* color2 = [NSColor colorWithDeviceWhite:1.0 alpha:0.10];
NSColor* color3 = [NSColor colorWithDeviceWhite:1.0 alpha:0.0];
// a location of 0.0 is the 'start' of the gradient; 1.0 is the 'end'.
NSGradient* sheen = [[NSGradient alloc] initWithColorsAndLocations:
color1, 0.0, color2, 0.4, color3, 0.8, nil];
return [sheen autorelease];
}
实现绘制光泽的方法:
- (void) drawImageSheenInRect:(NSRect)rect {
NSBezierPath* sheenPath = [NSBezierPath sheenPathForRect:rect];
// draw at 280.0 degees to simulate a light source from the upper-left.
[self.imageSheen drawInBezierPath:sheenPath angle:280.0];
}
在
drawRect:
方法中调用绘制光泽的方法:
// restore the context so the frame doesn't get a shadow.
[NSGraphicsContext restoreGraphicsState];
// draw the sheen.
[self drawImageSheenInRect:imageRect];
// draw a border around the image.
[self.borderColor set];
NSBezierPath* imageFrame = [NSBezierPath bezierPathWithRect:imageRect];
imageFrame.lineWidth = 4;
[imageFrame stroke];
最后,在
BasicCocoaDrawingAppDelegate.m
中激活光泽:
imageView.shouldAddShadow = YES;
imageView.backgroundGradient = gradient;
imageView.shouldAddSheen = YES;
保存所有文件并重新运行项目,你将看到图像上应用了光泽。
3. 代码重构的必要性和目标
随着代码的增加,代码变得难以理解和维护。此时,我们有三种选择:
-
允许代码混乱
:这是最简单的方法,但会导致代码难以调试和扩展。
-
重写代码
:虽然可以解决问题,但风险较大,可能会引入新的错误。
-
重构代码
:这是一种折中的方法,通过重新组织现有代码,使其更易于理解和维护。
重构
StyledImageView
类的目标包括:
- 使代码更易于阅读。
- 使视图更易于定制。
- 使方法更对称。
4. 重构示例:方法对称性
方法对称性是指具有相似功能的方法应该具有相似的名称和输入输出类型。例如,以下是不对称的代码:
- (NSArray*)itemsSortedByName;
- (NSArray*)dateSortedItems;
对称的版本如下:
- (NSArray*)itemsSortedByName;
- (NSArray*)itemsSortedByDate;
这样可以使代码更易于理解和使用。
5. 重构后的头文件
将
StyledImageView.h
替换为以下重构版本:
#import <Cocoa/Cocoa.h>
#pragma mark -
#pragma mark Delegate Protocols
// allows another class to calculate the geometry.
@protocol StyleImageViewGeometryDelegate <NSObject>
- (NSRect)imageRectForContentRect:(NSRect)contentRect;
@end
#pragma mark -
#pragma mark Class Definiton
@interface StyledImageView : NSView
#pragma mark -
#pragma mark Main Properties
// content.
@property (copy) NSImage* mainImage;
// colors and gradients.
@property (retain) NSGradient* backgroundGradient;
@property (retain) NSColor* backgroundColor;
@property (retain) NSColor* borderColor;
// display options.
@property (assign) BOOL shouldAddShadow;
@property (assign) BOOL shouldAddSheen;
// delegate.
@property (assign) id<StyleImageViewGeometryDelegate> delegate;
#pragma mark -
#pragma mark Drawing Methods
// drawing methods for subclasses to override.
- (void) drawBackground;
- (void) drawImage;
- (void) drawImageBorder;
- (void) drawImageSheen;
@end
这个头文件分为三个主要部分:委托协议、视图的公共属性和视图子类可以重写的方法。
6. 重构后的实现文件
重构
StyledImageView.m
如下:
#import "StyledImageView.h"
#import "NSImage-Utilities.h"
#import "NSBezierPath-Utilities.h"
@interface StyledImageView ()
// private properties.
@property (assign) NSRect contentRect;
@property (readonly) NSRect imageRect;
@property (retain) NSShadow* imageShadow;
@property (retain) NSGradient* imageSheen;
// private methods.
+ (NSShadow*) defaultImageShadow;
+ (NSGradient*) defaultImageSheen;
@end
@implementation StyledImageView
// content.
@synthesize mainImage;
@synthesize contentRect;
// colors and gradients.
@synthesize backgroundGradient;
@synthesize backgroundColor;
@synthesize borderColor;
// display options.
@synthesize shouldAddShadow;
@synthesize shouldAddSheen;
@synthesize imageShadow;
@synthesize imageSheen;
// delegate.
@synthesize delegate;
- (id)initWithFrame:(NSRect)frame {
if ( self = [super initWithFrame:frame] ) {
}
return self;
}
- (void) dealloc {
self.mainImage = nil;
self.backgroundGradient = nil;
self.backgroundColor = nil;
self.borderColor = nil;
self.imageShadow = nil;
self.imageSheen = nil;
self.delegate = nil;
[super dealloc];
}
访问器方法实现:
#pragma mark -
#pragma mark Property Accessors
- (void) setShouldAddShadow:(BOOL)shouldAdd {
if ( shouldAddShadow == shouldAdd ) return;
shouldAddShadow = shouldAdd;
if ( shouldAddShadow )
self.imageShadow = [[self class] defaultImageShadow];
else
self.imageShadow = nil;
[self setNeedsDisplay:YES];
}
- (void) setShouldAddSheen:(BOOL)shouldAdd {
if ( shouldAddSheen == shouldAdd ) return;
shouldAddSheen = shouldAdd;
if ( shouldAddSheen )
self.imageSheen = [[self class] defaultImageSheen];
else
self.imageSheen = nil;
[self setNeedsDisplay:YES];
}
#pragma mark -
#pragma mark Geometry
- (NSRect) imageRect {
// first, check with the delegate.
if ( [self.delegate respondsToSelector:@selector(imageRectForContentRect:)] )
return [self.delegate imageRectForContentRect:self.contentRect];
// if delegate didn't return a value, calculate it.
NSImage* image = self.mainImage;
return [image proportionalRectForTargetRect:self.contentRect];
}
绘制方法实现:
#pragma mark -
#pragma mark Drawing
- (void)drawRect:(NSRect)rect {
// set up the content rect that will be used by the other
// drawing methods.
NSRect bounds = self.bounds;
CGFloat insetX = NSWidth ( bounds ) * 0.10;
CGFloat insetY = NSHeight ( bounds ) * 0.10;
self.contentRect = NSInsetRect ( bounds, insetX, insetY );
// call each drawing method separately.
[self drawBackground];
[self drawImage];
[self drawImageBorder];
[self drawImageSheen];
}
- (void) drawBackground {
[self.backgroundColor set];
NSRectFill ( self.bounds );
[self.backgroundGradient drawInRect:self.bounds angle:90.0];
}
- (void) drawImage {
NSImage* image = self.mainImage;
[NSGraphicsContext saveGraphicsState];
[self.imageShadow set];
[image drawInRect: self.imageRect
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0];
[NSGraphicsContext restoreGraphicsState];
}
- (void) drawImageBorder {
[self.borderColor set];
NSBezierPath* imageFrame = [NSBezierPath bezierPathWithRect:self.imageRect];
imageFrame.lineWidth = 4;
[imageFrame stroke];
}
- (void) drawImageSheen {
NSRect rect = self.imageRect;
NSBezierPath* sheenPath = [NSBezierPath sheenPathForRect:rect];
// draw at 280.0 degrees to simulate a light source from the upper-left.
[self.imageSheen drawInBezierPath:sheenPath angle:280.0];
}
默认值方法:
#pragma mark -
#pragma mark Default Values
+ (NSShadow*) defaultImageShadow {
NSShadow* newShadow = [[NSShadow alloc] init];
newShadow.shadowBlurRadius = 8.0;
newShadow.shadowOffset = NSMakeSize(0,-6);
return newShadow;
}
通过以上步骤,我们不仅为图像视图添加了背景渐变和光泽效果,还对代码进行了重构,提高了代码的可读性和可维护性。在实际开发中,我们应该养成定期重构代码的习惯,以确保代码的质量和可扩展性。
自定义视图绘制与代码重构:打造高效美观的图像视图
7. 重构带来的好处分析
重构代码虽然需要花费一定的时间和精力,但从长远来看,它能带来诸多显著的好处。以下是详细分析:
7.1 易于调试
当代码结构清晰、方法和变量命名规范且具有对称性时,调试过程会变得更加高效。例如,在查找某个属性或方法的问题时,我们可以快速定位到相关代码,而不需要在混乱的代码中四处寻找。
7.2 便于他人理解
在团队协作开发或者将项目交接给其他开发者时,整洁的代码能够让他人迅速理解代码的功能和逻辑。如果代码杂乱无章,新接手的开发者可能需要花费大量时间去梳理代码,甚至可能因为理解错误而引入新的问题。
7.3 提高开发效率
干净的代码就像一条畅通的道路,能够让我们快速地添加新功能。相反,在杂乱的代码基础上添加新功能,就如同在泥泞的道路上行走,不仅速度慢,还容易陷入困境。例如,在重构后的
StyledImageView
类中,我们可以轻松地修改或添加绘制方法,而不用担心会影响到其他部分的代码。
7.4 避免遗忘代码逻辑
随着项目的发展和时间的推移,我们可能会忘记自己编写的代码的具体实现细节。重构后的代码具有良好的可读性和逻辑性,即使过了一段时间后再回过头来看,也能快速理解代码的意图。
以下是一个简单的表格,总结了重构代码的好处:
| 好处 | 描述 |
| — | — |
| 易于调试 | 快速定位问题,提高调试效率 |
| 便于他人理解 | 降低团队协作和项目交接的难度 |
| 提高开发效率 | 更轻松地添加新功能,减少错误发生的概率 |
| 避免遗忘代码逻辑 | 即使长时间后也能快速理解代码意图 |
8. 代码灵活性与性能的权衡
在重构代码的过程中,我们需要在代码的灵活性和性能之间进行权衡。例如,在
drawRect:
方法中,我们选择将
contentRect
保存为属性,而不是直接传递给每个绘制方法。虽然这样做会带来微小的性能损失,但却提高了代码的灵活性。
以下是
drawRect:
方法的代码示例:
- (void)drawRect:(NSRect)rect {
// set up the content rect that will be used by the other
// drawing methods.
NSRect bounds = self.bounds;
CGFloat insetX = NSWidth ( bounds ) * 0.10;
CGFloat insetY = NSHeight ( bounds ) * 0.10;
self.contentRect = NSInsetRect ( bounds, insetX, insetY );
// call each drawing method separately.
[self drawBackground];
[self drawImage];
[self drawImageBorder];
[self drawImageSheen];
}
如果我们直接将
contentRect
传递给每个方法,当需要修改传递的参数类型(如从
NSRect
改为
NSSize
)时,就需要修改所有相关的方法。而将其保存为属性,我们只需要在一处进行修改,其他方法仍然可以正常使用。
在实际开发中,我们应该根据具体的场景来决定如何权衡灵活性和性能。如果代码需要被其他开发者复用和定制,那么灵活性往往更为重要;如果对性能要求极高,那么可以适当牺牲一些灵活性来换取性能的提升。
9. 定制化与复杂度的平衡
在设计类时,我们希望类既易于使用,又具有一定的定制性。然而,过多的定制选项可能会导致类变得过于复杂,增加使用的难度。以下是一些关于平衡定制化和复杂度的建议:
9.1 明确核心功能
在设计类之前,我们需要明确类的核心功能是什么。例如,
StyledImageView
类的核心功能是显示图像,并添加一些特效(如背景渐变、光泽等)。我们应该围绕这些核心功能来设计类的接口和属性。
9.2 提供必要的定制选项
对于一些常见的定制需求,我们可以提供相应的属性和方法。例如,在
StyledImageView
类中,我们提供了
shouldAddShadow
和
shouldAddSheen
属性,让用户可以选择是否添加阴影和光泽效果。
9.3 避免过度定制
如果一个类有太多的属性和方法,用户在使用时可能会感到困惑。我们应该只提供那些真正必要的定制选项,避免让类变得过于复杂。例如,在
StyledImageView
类中,我们没有提供过多的绘制细节选项,而是将这些细节封装在类内部,让用户只需要关注一些关键的属性和方法。
以下是一个简单的 mermaid 流程图,展示了定制化和复杂度的平衡过程:
graph LR
A[明确核心功能] --> B[提供必要的定制选项]
B --> C{是否过度定制?}
C -- 是 --> D[减少定制选项]
C -- 否 --> E[完成设计]
D --> B
10. 总结
通过为图像视图添加背景渐变、光泽效果,并对代码进行重构,我们实现了高效美观的图像视图。在开发过程中,我们应该注重代码的质量和可维护性,定期对代码进行重构,以提高开发效率和代码的可扩展性。同时,我们需要在代码的灵活性和性能、定制化和复杂度之间找到平衡,以设计出既实用又易于使用的类。
在未来的开发中,我们可以进一步扩展
StyledImageView
类的功能,例如添加更多的特效、支持不同的图像格式等。同时,我们也可以将重构的经验应用到其他项目中,不断提高自己的开发水平。
超级会员免费看
582

被折叠的 条评论
为什么被折叠?



