UICollectionView에서 레이아웃을 동적으로 설정하면 설명할 수 없는 contentOffset 변경이 발생함
Apple의에, 의 레이아웃을 설정할 수 . 2012)에서 할 수 .UICollectionView
동적으로 그리고 심지어 변경사항을 애니메이션화합니다.
일반적으로 집합 뷰를 작성할 때 레이아웃 객체를 지정하지만 집합 뷰의 레이아웃을 동적으로 변경할 수도 있습니다.레이아웃 개체는 collectionViewLayout 속성에 저장됩니다.이 속성을 직접 설정하면 변경 사항을 애니메이션화하지 않고 레이아웃이 즉시 업데이트됩니다.변경 내용을 애니메이션화하려면 setCollectionViewLayout:animated: 메서드를 대신 호출해야 합니다.
하지만, 실제로, 저는 그것을 발견했습니다.UICollectionView
설명할 수 없고 심지어 유효하지 않은 변경을 가합니다.contentOffset
셀이 잘못 이동하여 기능을 사실상 사용할 수 없게 만듭니다.문제를 설명하기 위해 스토리보드에 드롭된 기본 컬렉션 뷰 컨트롤러에 첨부할 수 있는 다음 샘플 코드를 작성했습니다.
#import <UIKit/UIKit.h>
@interface MyCollectionViewController : UICollectionViewController
@end
@implementation MyCollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
[self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}
@end
으로 "" "" "" " ""를 합니다.UICollectionViewFlowLayout
viewDidLoad
화면에 단일 셀을 표시합니다.인 "" "" "" "" ""를 .UICollectionViewFlowLayout
를 사용하여 합니다.animated:YES
플래그. 예상되는 동작은 셀이 움직이지 않는 것입니다.그러나 실제 동작은 셀을 화면 밖으로 스크롤하는 것입니다. 이때 셀을 화면으로 다시 스크롤할 수도 없습니다.
콘솔 로그를 보면 내용 오프셋이 (내 프로젝트에서) (0, 0)에서 (0, 205)로 설명할 수 없이 변경되었음을 알 수 있습니다.비애니메이션 케이스에 대한 해결책을 게시했습니다 (i.e. animated:NO
), 하지만 저는 애니메이션이 필요하기 때문에 애니메이션 케이스에 대한 해결책이나 해결책이 있는지 알고 싶습니다.
참고로 사용자 지정 레이아웃을 테스트해 본 결과 동일한 동작을 수행했습니다.
UICollectionViewLayout
에는 레이아웃을 변경하는 동안 적절한 콘텐츠 오프셋을 제공할 수 있는 재정의 가능한 메서드가 포함되어 있으며, 이 메서드는 올바르게 애니메이션화됩니다.iOS 7.0 이상에서 사용할 수 있습니다.
저는 며칠 동안 이 문제로 머리를 쥐어짜고 있었고 도움이 될지도 모르는 제 상황에 대한 해결책을 찾았습니다.저의 경우 ipad의 사진 앱처럼 사진 레이아웃이 접힙니다.사진이 겹쳐진 앨범을 보여주고 앨범을 누르면 사진이 확대됩니다. 두 UICollectionViewLayouts를 사용하여 두 합니다.[self.collectionView setCollectionViewLayout:myLayout animated:YES]
애니메이션이 나오기 전에 세포가 점프하는 것에 대한 당신의 정확한 문제를 가지고 있었고 그것이contentOffset
저는 모든 것을 시도했습니다.contentOffset
은 효과가 으로 만들고 .위의 타일러 솔루션은 효과가 있었지만 여전히 애니메이션을 엉망으로 만들고 있었습니다.
그러다 보니 화면에 앨범이 몇 개 있을 때만 발생하고 화면을 가득 채울 정도는 아니라는 것을 알게 되었습니다.내 레이아웃이 오버라이드됨-(CGSize)collectionViewContentSize
권고대로앨범 수가 적을 때는 컬렉션 보기 콘텐츠 크기가 보기 콘텐츠 크기보다 작습니다.수집 레이아웃을 전환할 때 점프가 발생합니다.
그래서 레이아웃에 minHeight라는 속성을 설정하고 컬렉션 뷰 부모의 높이로 설정했습니다.그리고 나서 돌아가기 전에 키를 확인합니다.-(CGSize)collectionViewContentSize
높이가 >= 최소 높이가 되도록 합니다.
진정한 해결책은 아니지만 지금은 잘 작동하고 있습니다.▁the다를 설정해 보겠습니다.contentSize
컬렉션 뷰가 포함된 뷰의 길이 이상이어야 합니다.
edit: Manicaesar는 UICollectionViewFlowLayout에서 상속할 경우 쉬운 해결 방법을 추가했습니다.
-(CGSize)collectionViewContentSize { //Workaround
CGSize superSize = [super collectionViewContentSize];
CGRect frame = self.collectionView.frame;
return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}
2019년 실제 솔루션
"Cars" 보기를 위한 여러 레이아웃이 있다고 가정합니다.
예를 들어 세 개가 있다고 가정해 보겠습니다.
CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...
레이아웃 사이에 애니메이션을 만들면 점프합니다.
그것은 애플의 부정할 수 없는 실수일 뿐입니다.그것은 의심할 여지 없이 애니메이션을 만들 때 점프합니다.
해결책은 다음과 같습니다.
글로벌 플로트 및 다음 기본 클래스가 있어야 합니다.
var avoidAppleMessupCarsLayouts: CGPoint? = nil
class FixerForCarsLayouts: UICollectionViewLayout {
override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
avoidAppleMessupCarsLayouts = collectionView?.contentOffset
}
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
if avoidAppleMessupCarsLayouts != nil {
return avoidAppleMessupCarsLayouts!
}
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
다음은 "Cars" 화면의 세 가지 레이아웃입니다.
CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...
바로 그겁니다.이제 작동합니다.
믿을 수 없을 정도로 모호하게, 서로 다른 레이아웃 세트(자동차, 개, 집 등)가 충돌할 수 있습니다.이러한 이유로 각 "세트"에 대해 위와 같은 전역 및 기본 클래스를 사용합니다.
이것은 위의 사용자인 @Isaacliu에 의해 수년 전에 발명되었습니다.
자세한 건 아이작리우의 암호 조각에 있는 FWIW입니다
finalizeLayoutTransition
가 추가되었습니다.사실 그것은 논리적으로 필요하지 않습니다.
사실, Apple이 작동 방식을 변경하기 전까지는 컬렉션 뷰 레이아웃 사이에서 애니메이션을 만들 때마다 이 작업을 수행해야 합니다.그게 인생입니다!
이 문제는 저에게도 영향을 미쳤고 전환 코드의 버그인 것 같습니다.제가 볼 수 있는 바로는 전환 전 보기 레이아웃의 중심에 가장 가까운 셀에 초점을 맞추려고 합니다.그러나 보기 사전 전환의 중앙에 셀이 없는 경우에도 셀은 전환 후 셀이 있을 위치의 중심을 잡으려고 시도합니다.이는 항상 BounceVertical/Horizontal을 YES로 설정하고 단일 셀로 보기를 로드한 다음 레이아웃 전환을 수행하는 경우 매우 명확합니다.
레이아웃 업데이트를 트리거한 후 컬렉션에 특정 셀(이 예에서는 첫 번째 셀)에 초점을 맞추도록 명시적으로 지시함으로써 이 문제를 해결할 수 있었습니다.
[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];
// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
[self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
내 질문에 대한 늦은 대답으로 뛰어들기.
TLLayout Transitioning 라이브러리는 iOS7의 대화형 전환 API를 재작업하여 비대화형 레이아웃에서 레이아웃으로 전환함으로써 이 문제에 대한 훌륭한 해결책을 제공합니다.효과적으로 대안을 제공합니다.setCollectionViewLayout
콘텐츠 오프셋 문제 해결 및 몇 가지 기능 추가:
- 애니메이션 기간
- 30개 이상의 완화 곡선(워렌 무어의 AHEASING 라이브러리 제공)
- 다중 콘텐츠 오프셋 모드
사용자 정의 완화 곡선은 다음과 같이 정의할 수 있습니다.AHEasingFunction
Minimal), Top( Left(), Bottom(아래) Right() 배치 옵션을 하여 하나 이상의 할 수 .최종 내용 간격띄우기는 Minimal, Center, Top, Left, Bottom 또는 Right 배치 옵션을 사용하여 하나 이상의 인덱스 경로로 지정할 수 있습니다.
예 작업 공간에서 크기 조정 데모를 실행하고 옵션을 사용해 보십시오.
사용법은 이렇습니다.뷰 컨트롤러를 하여 먼저, 다의인반환의 합니다.TLTransitionLayout
:
- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}
그러면 전화하는 대신에.setCollectionViewLayout
, 전화 걸기transitionToCollectionViewLayout:toLayout
에정된에 UICollectionView-TLLayoutTransitioning
범주:
UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];
으로 " " " "라는 메시지가 표시됩니다.CADisplayLink
지정된 지속 시간과 완화 기능으로 전환 진행을 유도하는 콜백.
다음 단계에서는 최종 내용 오프셋을 지정합니다.값을 할 수 , "" "" " " " " " " "는,toContentOffsetForLayout
「 」에 UICollectionView-TLLayoutTransitioning
하나 이상의 인덱스 경로에 상대적인 내용 오프셋을 계산하는 우아한 방법을 제공합니다.를 들어, 다음 에 걸면 .transitionToCollectionViewLayout
:
NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement];
layout.toContentOffset = toOffset;
만만하다.
새 레이아웃 및 컬렉션을 애니메이션화 동일한 애니메이션 블록에서 View의 컨텐츠 오프셋을 봅니다.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
[self.collectionView setContentOffset:CGPointMake(0, -64)];
} completion:nil];
그것은 계속될 것입니다.self.collectionView.contentOffset
일정한.
레이아웃에서 전환할 때 변경되지 않는 내용 오프셋을 찾는 경우 사용자 지정 레이아웃을 만들고 몇 가지 방법을 재정의하여 이전 내용 오프셋을 추적하고 재사용할 수 있습니다.
@interface CustomLayout ()
@property (nonatomic) NSValue *previousContentOffset;
@end
@implementation CustomLayout
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGPoint previousContentOffset = [self.previousContentOffset CGPointValue];
CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ;
}
- (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout
{
self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset];
return [super prepareForTransitionFromLayout:oldLayout];
}
- (void)finalizeLayoutTransition
{
self.previousContentOffset = nil;
return [super finalizeLayoutTransition];
}
@end
이 모든 것은 레이아웃 전환 전에 이전 내용 오프셋을 저장하는 것입니다.prepareForTransitionFromLayout
새내 오덮어기쓰에서 새 내용 targetContentOffsetForProposedContentOffset
그리고 그것을 치우는 것.finalizeLayoutTransition
직설적입니다.
경험을 쌓는 데 도움이 된다면 다음과 같습니다.저는 콘텐츠의 크기, 콘텐츠를 설정했는지 여부, 또는 다른 명백한 요소에 관계없이 이 문제에 지속적으로 직면했습니다.그래서 저의 해결책은 다소 과격했습니다.먼저 UICollectionView를 하위 분류하고 부적절한 콘텐츠 오프셋 설정을 방지하기 위해 추가했습니다.
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
}
- (void)setContentSize:(CGSize)contentSize
{
_declineContentOffset ++;
[super setContentSize:contentSize];
_declineContentOffset --;
}
자랑스럽지는 않지만 실행 가능한 유일한 해결책은 콜렉션 뷰에 의한 콘텐츠 오프셋 설정 시도를 완전히 거부하는 것 같습니다.setCollectionViewLayout:animated:
경험적으로 이러한 변화는 즉각적인 통화에서 직접 발생하는 것으로 보이며, 이는 분명히 인터페이스나 설명서에 의해 보장되지는 않지만 핵심 애니메이션의 관점에서는 타당하기 때문에 가정에 대해 50%만 불편할 수 있습니다.
그러나 두 번째 문제가 있었습니다.이제 UICollectionView는 새로운 컬렉션 뷰 레이아웃에서 동일한 위치에 있던 뷰에 약간의 점프를 추가하여 약 240포인트를 낮춘 다음 원래 위치로 애니메이션화했습니다.이유는 확실하지 않지만 그럼에도 불구하고 나는 그것을 처리하기 위해 내 코드를 수정했습니다.CAAnimation
실제로 움직이지 않는 모든 세포에 추가된 s:
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
// collect up the positions of all existing subviews
NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
for(UIView *view in [self subviews])
{
positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
}
// apply the new layout, declining to allow the content offset to change
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
// run through the subviews again...
for(UIView *view in [self subviews])
{
// if UIKit has inexplicably applied animations to these views to move them back to where
// they were in the first place, remove those animations
CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"];
NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
{
NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];
if([targetValue isEqualToValue:sourceValue])
[[view layer] removeAnimationForKey:@"position"];
}
}
}
이는 실제로 움직이는 뷰를 억제하거나 잘못 이동하게 하지는 않는 것으로 보입니다(주변의 모든 것이 약 240포인트 하락하여 올바른 위치로 애니메이션을 만들 것으로 예상하는 것처럼 보입니다).
이것이 제 현재 해결책입니다.
저는 아마 2주 동안 다양한 레이아웃을 서로 원활하게 전환하기 위해 노력했습니다.iOS 10.2에서는 제안된 오프셋을 재정의하는 기능이 작동하지만 그 이전 버전에서는 여전히 문제가 발생합니다.제 상황을 좀 더 악화시키는 것은 스크롤의 결과로 다른 레이아웃으로 전환해야 하기 때문에 보기가 스크롤과 전환을 동시에 수행할 수 있다는 것입니다.
Tommy의 답변은 10.2 이전 버전에서 저에게 유일하게 도움이 되었습니다.저는 지금 다음과 같은 일을 하고 있습니다.
class HackedCollectionView: UICollectionView {
var ignoreContentOffsetChanges = false
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setContentOffset(contentOffset, animated: animated)
}
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentOffset = newValue
}
}
override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setCollectionViewLayout(layout, animated: animated)
}
override var contentSize: CGSize {
get {
return super.contentSize
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentSize = newValue
}
}
}
그리고 레이아웃을 설정할 때 이렇게 합니다.
let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100)
UIView.animate(withDuration: animationDuration,
delay: 0, options: animationOptions,
animations: {
collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in
// I'm also doing something in my layout, but this may be redundant now
layout.overriddenContentOffset = nil
})
collectionView.ignoreContentOffsetChanges = true
}, completion: { _ in
collectionView.ignoreContentOffsetChanges = false
collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false)
})
이것은 마침내 나에게 효과가 있었습니다 (스위프트 3)
self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)
언급URL : https://stackoverflow.com/questions/13780138/dynamically-setting-layout-on-uicollectionview-causes-inexplicable-contentoffset
'programing' 카테고리의 다른 글
VBA 코드를 사용하여 데이터 복사 및 붙여넣기 (0) | 2023.08.23 |
---|---|
UITextView에 커서가 표시되지 않음 (0) | 2023.08.23 |
ASP의 경우 "모델을 만드는 동안에는 컨텍스트를 사용할 수 없습니다." 예외입니다.NET 아이덴티티 (0) | 2023.08.23 |
UIView에서 두 모서리를 둥글게 만듭니다. (0) | 2023.08.23 |
re."예기된 문자열 또는 바이트와 유사한 개체"로 하위 오류가 발생 (0) | 2023.08.23 |