如何构建一个包含图像的视差样式/可拉伸的表格视图标题,而不会出现丑陋的黑客攻击?

Posted

技术标签:

【中文标题】如何构建一个包含图像的视差样式/可拉伸的表格视图标题,而不会出现丑陋的黑客攻击?【英文标题】:How do I build a parallax-style/stretchy table view header with an image in it without ugly hacks? 【发布时间】:2015-01-03 06:01:57 【问题描述】:

我一直在尝试构建一个由图像组成的视差样式表视图标题,类似于 Yahoo News Digest 应用程序,或者在 Maps.app 中查看业务时。 (当您用橡皮筋固定表格时,图像高度会增加,而向下滚动时,图像的滚动速度会稍慢)。

这是由APParallaxHeader提供的演示视频:

https://www.youtube.com/watch?v=7-JMdapWXGU

我能找到的最好的教程是this tutorial,它基本上包括将图像视图添加为表格视图的子视图。虽然这主要是可行的,但作为子视图添加到 UITableView 是相当无证的,并且在我的测试中似乎不适用于自动布局,因此旋转不能很好地发挥作用。

我在上面链接的库 APParallaxHeader 似乎可以工作,但它的实现真的很混乱,如果我没记错的话,似乎很混乱?

有没有一种我完全忽略的简单方法?

【问题讨论】:

在我看来,您可以使用表头视图来做到这一点。您需要在滚动时调整其高度,并在其中拉伸图像视图(修改其约束或应用变换)。 @rdelmar 你确定吗?当表格视图本身被拉下时,您将如何创建橡皮筋高度效果? @DougSmith 高度和缩放量的控制参数是表格的 contentOffset.y。该值的变化与反弹非常接近。听起来像是一个有趣的练习。 (我同意你的观点,它看起来应该很简单)。 【参考方案1】:

在考虑了这个问题之后,我认为复制该外观的最佳方法是使用滚动视图,该滚动视图包含一个图像视图,该视图位于后面(在 z 方向上)并在下方(在 y 方向上)延伸表视图的顶部。在我所做的测试中,我为表格视图提供了一个 100 磅高的标题(在 IB 中),并且具有清晰的背景颜色(表格也需要清晰的背景颜色)。滚动视图和表格视图都固定在控制器主视图的侧面和顶部布局指南(控制器嵌入在导航控制器中,该控制器设置为使其视图不位于顶部栏下方)。表格视图也固定在视图的底部,并且滚动视图被赋予了 200 的固定高度。我给滚动视图的初始偏移量为 50 点,这样当您开始向下拉表格时,滚动视图可以从顶部将更多内容滚动到视图中,同时在底部显示更多内容(滚动视图的偏移量以表格视图偏移量的 1/2 速率移动)。一旦表格视图的偏移量达到 -50,我就停止更改滚动视图的偏移量,并开始缩放。

#define ZOOMPOINT 50

@interface ViewController () <UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *sv;
@property(weak,nonatomic) IBOutlet UITableView *tableView;
@property (strong,nonatomic) UIImageView *iv;
@end

@implementation ViewController

-(void)viewDidLoad 
    [super viewDidLoad];
    self.sv.minimumZoomScale = 1.0;
    self.sv.maximumZoomScale = 2.0;
    self.sv.delegate = self;
    self.iv = [UIImageView new];
    self.iv.contentMode = UIViewContentModeScaleAspectFill;
    self.iv.translatesAutoresizingMaskIntoConstraints = NO;




-(void)viewDidLayoutSubviews 
    [self.iv removeFromSuperview];
    [self.sv addSubview:self.iv];
    [self.sv addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[iv(==width)]|" options:0 metrics:@@"width":@(self.tableView.frame.size.width) views:@@"iv":self.iv]];
    [self.sv addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[iv(==250)]|" options:0 metrics:nil views:@@"iv":self.iv]];
    self.iv.image = [UIImage imageNamed:@"img.jpg"]; // the image I was using was 500 x 500
    self.sv.contentOffset = CGPointMake(0, ZOOMPOINT);


- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView 
    if ([scrollView isEqual:self.sv]) 
        return self.iv;
    else
        return nil;
    



- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale 




- (void)scrollViewDidScroll:(UIScrollView *)scrollView 
    if (scrollView != self.sv) 
        if (scrollView.contentOffset.y < -ZOOMPOINT) 
            [self.sv setZoomScale:(scrollView.contentOffset.y + ZOOMPOINT)/-100 + 1]; // the -100 is arbitrary, change to affect the sensitivity of the zooming
        else
             self.sv.contentOffset = CGPointMake(0, ZOOMPOINT + scrollView.contentOffset.y/2.0);
        
    




- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
    return 20;


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    cell.textLabel.text = [NSString stringWithFormat:@"Cell %ld", (long)indexPath.row];
    return cell;

我在这里上传了这个项目的副本,http://jmp.sh/LRKF0nM

【讨论】:

干得好!布局约束(我懒惰地省略了)处理 OP 对旋转的要求。我的行为类似,但是在滚动时,它将图像滚动而不是缩小(因为它在表格内)。但我更喜欢依赖较少常量的方法。 jumpshare.com/v/aE6JyBjLLQJ0LpJjVQVv?b=FCoUUjKBOTi3tknKEgsf 很好。但是对于您的图像,最好在缩放之前让它扩展整个高度,以改变图像视图的高度通常是如何操作的。此外,当您在表格视图上拉得太远并且标题不断缩放而不改变其高度时,它显然看起来很奇怪。这是可以避免的吗? @DougSmith,当然,您可以调整它在缩放之前滚动的距离。至于第二点,我相信你也可以通过弄乱参数来改变它。您已经获得了滚动视图的高度、标题视图的高度以及滚动视图的初始偏移量。【参考方案2】:

我想我会抛出另一个不使用单独滚动视图的想法。我认为它的扩展方式会更好一些。因此,在这次尝试中,我只是将图像视图添加为主视图的子视图,并将其放置为图像视图的 1/2 位于标题顶部(视图外)上方,与标题下方(最初被表格行隐藏)。下拉表格时,视图以下拉速度的一半向下移动(通过调整约束),因此图像的顶部和底部一起进入视图,然后从那里,我通过使用进行扩展变换。

#import "ViewController.h"
#define ZOOMPOINT -60

@interface ViewController () <UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate>
@property(weak,nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UIView *tableHeader;

@property (strong,nonatomic) UIImageView *iv;
@property (strong,nonatomic) NSLayoutConstraint *topCon;
@end

@implementation ViewController

-(void)viewDidLoad 
    [super viewDidLoad];
    self.iv = [UIImageView new];
    self.iv.contentMode = UIViewContentModeScaleToFill; //UIViewContentModeScaleAspectFill;
    self.iv.translatesAutoresizingMaskIntoConstraints = NO;
    self.edgesForExtendedLayout = UIRectEdgeNone;



-(void)viewDidAppear:(BOOL)animated 
    [super viewDidAppear:animated];
    [self.view addSubview:self.iv];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[iv]|" options:0 metrics:nil views:@@"iv":self.iv]];
    self.topCon = [NSLayoutConstraint constraintWithItem:self.iv attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:ZOOMPOINT/2.0];
    [self.iv addConstraint:[NSLayoutConstraint constraintWithItem:self.iv attribute:NSLayoutAttributeHeight relatedBy:0 toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:self.tableHeader.frame.size.height - ZOOMPOINT*1.5]];
    [self.view addConstraint:self.topCon];
    [self.view layoutIfNeeded];

    self.iv.image = [UIImage imageNamed:@"img.jpg"];
    [self.view sendSubviewToBack:self.iv];




- (void)scrollViewDidScroll:(UIScrollView *)scrollView 
    if (scrollView.contentOffset.y < 0 && scrollView.contentOffset.y > ZOOMPOINT) 
        self.topCon.constant = ZOOMPOINT/2.0 - scrollView.contentOffset.y/2.0;
    else if (scrollView.contentOffset.y <= ZOOMPOINT) 
        self.iv.transform = CGAffineTransformMakeScale(1 - (scrollView.contentOffset.y - ZOOMPOINT)/200, 1 - (scrollView.contentOffset.y - ZOOMPOINT)/200);
    




- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
    return 20;




- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    cell.textLabel.text = [NSString stringWithFormat:@"Cell %ld", (long)indexPath.row];
    return cell;

项目可以在这里找到,http://jmp.sh/7PXzISZ

【讨论】:

【参考方案3】:

您好,这是我关于该主题的中等帖子:

https://medium.com/@jeremysh/creating-a-sticky-header-for-a-uitableview-40af71653b55#.hi79wgtsd

【讨论】:

【参考方案4】:

这里只是重复,但是如果标题的自然框架是frame,并且您已经设置了表格的滚动视图委托,那么缩放的框架将非常类似于:

// in scrollViewDidScroll:
// when the table view is scrolled beyond the header, contentOffset.y is negative
CGFloat headerAspect = frame.size.width / frame.size.height;
CGFloat offsetY = tableView.contentOffset.y;
CGFloat offsetX = offsetY * headerAspect;

// this will enlarge frame since offsets are < 0
frame = CGInsetRect(frame, offsetY, offsetX);

// slide it down to keep the top at the top of the header
frame = CGRectOffset(frame, 0, offsetY / 2.0);

将此与将图像视图上的 contentMode 设置为 UIViewContentModeScaleToFill 相结合,这应该是一个不错的开始。

【讨论】:

我试过这段代码,但它似乎根本不起作用。我使用标题视图的主要问题(我可能对此有误,但它似乎也出现在此代码中)是当您下拉表格视图“橡皮筋”时,表格视图及其标题被物理翻译向下也是如此,所以标题不必在表格视图的边界之外绘制吗? @DougSmith,当滚动视图滚动时,它的子视图相对于它们的父视图(滚动视图)保持固定。子视图确实相对于滚动视图的超级视图移动,但这并不重要。让我多考虑一下。我可能会尝试在今天或明天真正实现这一点。如果我这样做,将在此处更新答案。 哦,好吧,这是有道理的。如果您有时间,我将不胜感激! @DougSmith - 在下面查看【参考方案5】:

好的,这是一个有助于构建和试用的答案。

我发现操作表格的实际标题视图的框架太难了,所以我在行上方的表格中添加了一个子视图。为了使该视图显示为常规表格标题,我为表格提供了一个固定大小、透明颜色的标题视图。

主要思路和我上面回答的一样:使用表格的内容偏移量作为修改图像视图框架的参数,使用imageView的内容模式(修正为UIViewContentModeScaleAspectFill)来提供随着框架变化的缩放效果。

这是整个视图控制器。这是从故事板构建的,其中视图控制器位于导航控制器内部。它只不过是一个填充其视图的表格视图,其中包含数据源和委托集。

#import "ViewController.h"

// how much of the image to show when the table is un-scrolled
#define HEADER_HEIGHT (100.0)

// the height of the image scaled down to fit in the header.  the real image can/should be taller than this
// i tested this with a 600x400 image
#define SCALED_IMAGE_HEIGHT (200.0)

// zoom image up to this offset
#define MAX_ZOOM (150.0)

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@property(weak,nonatomic) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (void)viewWillAppear:(BOOL)animated 
    [super viewWillAppear:animated];

    // build the header in view will appear after other layout constraints are applied
    UIImageView *headerView = (UIImageView *)[self.tableView viewWithTag:99];
    if (!headerView) 
        headerView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"landscape.png"]];
        headerView.tag = 99;
        headerView.contentMode = UIViewContentModeScaleAspectFill;
        headerView.clipsToBounds = YES;

        headerView.frame = CGRectMake(0, HEADER_HEIGHT, self.view.bounds.size.width, SCALED_IMAGE_HEIGHT);
        [self.tableView addSubview:headerView];
    


- (void)scrollViewDidScroll:(UIScrollView *)scrollView 

    CGFloat offsetY = -self.tableView.contentOffset.y - 64;
    // minus 64 is kind of a bummer here.  this calc wants the offset to be 0
    // when no scrolling has happened.  for some reason my table view starts at -64

    CGFloat clamped = MIN(MAX(offsetY, 0), MAX_ZOOM);
    CGFloat origin = -HEADER_HEIGHT - clamped;
    CGFloat height = SCALED_IMAGE_HEIGHT + clamped;

    UIImageView *headerView = (UIImageView *)[self.tableView viewWithTag:99];

    CGRect frame = headerView.frame;
    frame.origin.y = origin;
    frame.size.height = height;
    headerView.frame = frame;


// this is a trick to make the view above the header visible: make the table header a clear UIView
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section 
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, HEADER_HEIGHT)];
    view.backgroundColor = [UIColor clearColor];
    return view;


- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section 
    return HEADER_HEIGHT;


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
    return 30;


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    cell.textLabel.text = [NSString stringWithFormat:@"Cell %ld", indexPath.row];
    return cell;


@end

【讨论】:

嗯,我想我明白你在做什么了,但这是否适用于旋转和导航栏?这是我遇到的主要问题。 导航栏 - 是的,我也是这样建造的。旋转也可以,但您需要一张在两个方向上看起来都不错的图像(即,对于横向来说足够宽,但在纵向的顶部和底部有足够有用的内容)。 它是如何轮换工作的?您将高度距离硬编码为 64,这仅适用于纵向。状态栏高度 + 导航栏高度的 64 个结果。横向隐藏状态栏,导航栏高度不同,所以这个数字不再有效。 你能上传项目吗? 啊,你让它与顶部对齐,而不是 topLayoutGuide/nav 栏。这会减少很多图像。有没有办法避免这种情况?它也不适用于横向。

以上是关于如何构建一个包含图像的视差样式/可拉伸的表格视图标题,而不会出现丑陋的黑客攻击?的主要内容,如果未能解决你的问题,请参考以下文章

UIButton - 仅在一个方向上可拉伸的背景图像?

可拉伸的 UIButton 图像

如何在 Javascript 中制作可拉伸的聊天气泡?

Unity添加可拉伸的图片

用图像视图掩盖图像视图

三星最新屏幕黑科技:可拉伸的OLED屏,能贴在皮肤上