Wyśrodkuj zawartość UIScrollView, gdy jest mniejsza

140

Mam UIImageViewwewnątrzUIScrollView którego używam do powiększania i przewijania. Jeśli obraz / zawartość widoku przewijania jest większa niż widok przewijania, wszystko działa poprawnie. Jednak gdy obraz staje się mniejszy niż widok przewijania, przykleja się do lewego górnego rogu widoku przewijania. Chciałbym, aby był wyśrodkowany, tak jak aplikacja Zdjęcia.

Jakieś pomysły lub przykłady dotyczące utrzymania treści w UIScrollViewśrodku, gdy jest mniejsza?

Pracuję z iPhone'em 3.0.

Poniższy kod prawie działa. Obraz wraca do lewego górnego rogu, jeśli uszczypnę go po osiągnięciu minimalnego poziomu powiększenia.

- (void)loadView {
    [super loadView];

    // set up main scroll view
    imageScrollView = [[UIScrollView alloc] initWithFrame:[[self view] bounds]];
    [imageScrollView setBackgroundColor:[UIColor blackColor]];
    [imageScrollView setDelegate:self];
    [imageScrollView setBouncesZoom:YES];
    [[self view] addSubview:imageScrollView];

    UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"WeCanDoIt.png"]];
    [imageView setTag:ZOOM_VIEW_TAG];
    [imageScrollView setContentSize:[imageView frame].size];
    [imageScrollView addSubview:imageView];

    CGSize imageSize = imageView.image.size;
    [imageView release];

    CGSize maxSize = imageScrollView.frame.size;
    CGFloat widthRatio = maxSize.width / imageSize.width;
    CGFloat heightRatio = maxSize.height / imageSize.height;
    CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;

    [imageScrollView setMinimumZoomScale:initialZoom];
    [imageScrollView setZoomScale:1];

    float topInset = (maxSize.height - imageSize.height) / 2.0;
    float sideInset = (maxSize.width - imageSize.width) / 2.0;
    if (topInset < 0.0) topInset = 0.0;
    if (sideInset < 0.0) sideInset = 0.0;
    [imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return [imageScrollView viewWithTag:ZOOM_VIEW_TAG];
}

/************************************** NOTE **************************************/
/* The following delegate method works around a known bug in zoomToRect:animated: */
/* In the next release after 3.0 this workaround will no longer be necessary      */
/**********************************************************************************/
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    [scrollView setZoomScale:scale+0.01 animated:NO];
    [scrollView setZoomScale:scale animated:NO];
    // END Bug workaround

    CGSize maxSize = imageScrollView.frame.size;
    CGSize viewSize = view.frame.size;
    float topInset = (maxSize.height - viewSize.height) / 2.0;
    float sideInset = (maxSize.width - viewSize.width) / 2.0;
    if (topInset < 0.0) topInset = 0.0;
    if (sideInset < 0.0) sideInset = 0.0;
    [imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}
hpique
źródło
Czy kiedykolwiek całkowicie rozwiązałeś ten problem? Zmagam się z tym samym problemem.
Jonah
1
Uwaga: Użyj początkowej wartości Zoom do obliczenia wstawki, jeśli NIE jest jedną (bez powiększania). Np. Użyj tych linii: float topInset = (maxSize.height - imageSize.height * initialZoom) / 2.0; float sideInset = (maxSize.width - imageSize.width * initialZoom) / 2.0; i na koniec ustaw początkową wartość powiększenia [imageScrollView setZoomScale: initialZoom];
Felix

Odpowiedzi:

228

Mam bardzo proste rozwiązanie! Wszystko, czego potrzebujesz, to zaktualizować środek widoku podrzędnego (widok obrazu) podczas powiększania ScrollViewDelegate. Jeśli powiększony obraz jest mniejszy niż widok przewijania, dostosuj subview.center w przeciwnym razie środek jest (0,0).

- (void)scrollViewDidZoom:(UIScrollView *)scrollView 
{
    UIView *subView = [scrollView.subviews objectAtIndex:0];

    CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
    CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);

    subView.center = CGPointMake(scrollView.contentSize.width * 0.5 + offsetX, 
                                 scrollView.contentSize.height * 0.5 + offsetY);
}
Erdemus
źródło
5
Dla mnie to rozwiązanie jest bardziej szarpane niż użycie NYOBetterZoom Liama. Może zależy to od rozmiaru obrazu itp. Morał; skorzystaj z rozwiązania, które najlepiej odpowiada Twoim potrzebom
wuf810
2
Stackoverflow GOLD STAR za to. Walczyłem z tym, ale rozwiązanie jest takie proste.
n13
2
trochę uprościć:CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
Marek R
6
Ta korekta pomogła mi, gdy widok przewijania miał wstawki: CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentInset.left - scrollView.contentInset.right - scrollView.contentSize.width) * 0.5, 0.0); CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom - scrollView.contentSize.height) * 0.5, 0.0);
Kieran Harper,
3
Zauważyłem, że ta, podobnie jak wiele innych technik centrowania, wydaje się powodować problemy podczas używania zoomToRect:. Zastosowanie tego contentInsetpodejścia działa lepiej, jeśli potrzebujesz tej funkcji. Więcej informacji znajdziesz na petersteinberger.com/blog/2013/how-to-center-uiscrollview .
Matej Bukovinski
52

Odpowiedź @ EvelynCordner była najlepsza w mojej aplikacji. Dużo mniej kodu niż inne opcje.

Oto wersja Swift, jeśli ktoś jej potrzebuje:

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0, 0)
}
William T.
źródło
4
Ten działa świetnie! Po zmniejszeniu widok nawet prawidłowo się animował.
Carter Medlin
4
Umieściłem ten kod w func, a także nazwałem go w viewDidLayoutSubviews, aby upewnić się, że został poprawnie skonfigurowany na początku.
Carter Medlin
Dobra rozmowa telefoniczna z @CarterMedlin bardzo mi pomogła przy pierwszym ładowaniu Swift 3.
AJ Hernandez
Wydaje się, że to działa, ale nie rozumiem dlaczego, ponieważ zawsze wydaje się, że oblicza te same wstawki: granice widoku przewijania są stałe, podobnie jak rozmiar zawartości.
Vaddadi Kartick
contentSize zmienia się przy powiększaniu scrollView tylko rozmiar widoku jest naprawiony
Michał Ziobro
25

Okej, walczyłem z tym przez ostatnie dwa dni z przerwami i w końcu doszedłem do całkiem niezawodnego (jak dotąd ...) rozwiązania, pomyślałem, że powinienem się nim podzielić i zaoszczędzić innym trochę bólu. :) Jeśli znajdziesz problem z tym rozwiązaniem, krzycz!

Zasadniczo przejrzałem to, co wszyscy inni: przeszukując StackOverflow, fora programistów Apple, spojrzałem na kod dla three20, ScrollingMadness, ScrollTestSuite itp. Próbowałem powiększyć ramkę UIImageView, bawiąc się przesunięciem i / lub wstawkami UIScrollView z ViewController itp., ale nic nie działało świetnie (jak wszyscy inni też się przekonali).

Po spaniu na nim wypróbowałem kilka alternatywnych ujęć:

  1. Podklasowanie UIImageView, aby dynamicznie zmieniał swój rozmiar - to wcale nie działało dobrze.
  2. Podklasowanie UIScrollView tak, aby zmieniał on swoją własną zawartość dynamicznie Offset - to wydaje się być dla mnie zwycięzcą.

Dzięki tej podklasowej metodzie UIScrollView nadpisuję mutator contentOffset, więc nie ustawia {0,0}, gdy obraz jest skalowany mniejszy niż rzutnia - zamiast tego ustawia przesunięcie tak, aby obraz był wyśrodkowany w rzutni. Jak dotąd wydaje się, że zawsze działa. Sprawdziłem to z szerokimi, wysokimi, małymi i dużymi obrazami i nie ma problemu „działa, ale szczypanie przy minimalnym powiększeniu psuje”.

Wgrałem przykładowy projekt na github, który używa tego rozwiązania, możesz go znaleźć tutaj: http://github.com/nyoron/NYOBetterZoom

Liam Jones
źródło
2
Dla zainteresowanych - zaktualizowałem powyższy powiązany projekt, więc jest nieco mniej zależny od ViewController wykonującego `` właściwe rzeczy '', niestandardowy UIScrollView sam dba o więcej szczegółów.
Liam Jones
2
Liam, rządzisz. Mówię to jako autor ScrollingMadness. BTW na iPadzie w wersji 3.2+ ustawianie contentInset w scrollViewDidZoom (nowa metoda delegata 3.2+) Po prostu działa.
Andrey Tarantsov
Bardzo zgrabny fragment kodu. Od jakiegoś czasu drapię się tym po głowie. Dodano projekt do handyiphonecode.com
samvermette
To jest świetne. Dziękuję Ci. Nie rekompensowało to jednak ukrywania mojego UIStatusBar, więc zmieniłem linię anOffset.y = -(scrollViewSize.height - zoomViewSize.height) / 2.0naanOffset.y = (-(scrollViewSize.height - zoomViewSize.height) / 2.0) + 10;
Gordon Fontenot
Podane rozwiązanie bi Erdemus jest moim zdaniem znacznie prostsze.
gyozo kudor
23

Ten kod powinien działać na większości wersji iOS (i został przetestowany pod kątem działania od wersji 3.1).

Jest oparty na kodzie Apple WWDC dla fotokolera.

Dodaj poniższy kod do swojej podklasy UIScrollView i zastąp tileContainerView widokiem zawierającym obraz lub kafelki:

- (void)layoutSubviews {
    [super layoutSubviews];

    // center the image as it becomes smaller than the size of the screen
    CGSize boundsSize = self.bounds.size;
    CGRect frameToCenter = tileContainerView.frame;

    // center horizontally
    if (frameToCenter.size.width < boundsSize.width)
        frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
    else
        frameToCenter.origin.x = 0;

    // center vertically
    if (frameToCenter.size.height < boundsSize.height)
        frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
    else
        frameToCenter.origin.y = 0;

    tileContainerView.frame = frameToCenter;
}
JosephH
źródło
3
to powinna być akceptowana odpowiedź, bo jest prostsza. Dzięki. Uratowałeś mi życie!!!!!! : D
Duck
Po usunięciu wszystkich wywołań API iOS 3.2+ logika centrowania wydaje się działać tylko na urządzeniach z iOS 3.2+, a nie 3.1.3 (skoki, migotanie, otrzymuje losowe przesunięcie). Porównałem dane wyjściowe pochodzenia i rozmiaru ramki między 3.1.3 a 3.2+ i mimo że z jakiegoś powodu pasują do siebie, widok podrzędny nadal jest nieprawidłowo ustawiony. Bardzo dziwne. Tylko odpowiedź Liama ​​Jone'a zadziałała dla mnie.
David H
1
Zarówno rozwiązanie @Erdemus, jak i @JosephH działają, ale UIScrollViewpodejście podklasy wydaje się lepsze: jest wywoływane, gdy widok jest wyświetlany po raz pierwszy + w sposób ciągły podczas powiększania (podczas gdy scrollViewDidZoomjest wywoływany tylko raz na przewijanie i po fakcie)
SwiftArchitect
22

Aby uzyskać rozwiązanie lepiej dostosowane do widoków przewijania, które używają automatycznego układu, użyj wstawek zawartości widoku przewijania, zamiast aktualizować ramki widoków podrzędnych widoku przewijania.

- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
    CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);

    self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
Evelyn Cordner
źródło
1
To zadziałało dla mnie, jednak jeśli obraz jest mniejszy, to w jakiś sposób ten kod (przy bezpośrednim wywołaniu) nie aktualizował widoku przewijania. Co było mi potrzebne todo najpierw umieścić UIViewczyli wewnątrz UIScrollviewdo tego samego środka ( view.center = scrollview.center;), a następnie w scrollViewDidZoomsecie view.frame.origin xi yaby 0ponownie.
morksinaanab
U mnie też działa dobrze, ale przeszedłem dispatch_asyncdo głównej kolejki wviewWillAppear której wywołuję scrollViewDidZoom:mój główny widok przewijania. To sprawia, że ​​widok pojawia się z wyśrodkowanymi obrazami.
Pascal
Wywołaj ten kod w programie, viewDidLayoutSubviewsaby upewnić się, że jest on poprawnie skonfigurowany, gdy widok jest wyświetlany po raz pierwszy.
Carter Medlin
21

Obecnie tworzę podklasy UIScrollViewi nadpisuję, setContentOffset:aby dostosować przesunięcie na podstawie contentSize. Działa zarówno z zoomem szczypiącym, jak i programowym.

@implementation HPCenteringScrollView

- (void)setContentOffset:(CGPoint)contentOffset
{
    const CGSize contentSize = self.contentSize;
    const CGSize scrollViewSize = self.bounds.size;

    if (contentSize.width < scrollViewSize.width)
    {
        contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0;
    }

    if (contentSize.height < scrollViewSize.height)
    {
        contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0;
    }

    [super setContentOffset:contentOffset];
}

@end

Oprócz tego, że jest krótki i słodki, ten kod zapewnia znacznie płynniejszy zoom niż rozwiązanie @Erdemus. Możesz zobaczyć to w akcji w demo RMGallery .

hpique
źródło
6
Nie musisz tworzyć podklasy UIScrollView, aby zaimplementować tę metodę. Apple umożliwia wyświetlanie zdarzeń przewijania i powiększania w metodach delegowanych. W szczególności: - (void) scrollViewDidZoom: (UIScrollView *) scrollView;
Rog
1
Najlepsze rozwiązanie, jakie widziałem (działa nawet z ograniczeniami).
Frizlab
12

Spędziłem dzień walcząc z tym problemem i ostatecznie zaimplementowałem scrollViewDidEndZooming: withView: atScale: w następujący sposób:

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
    CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
    CGFloat viewWidth = view.frame.size.width;
    CGFloat viewHeight = view.frame.size.height;

    CGFloat x = 0;
    CGFloat y = 0;

    if(viewWidth < screenWidth) {
        x = screenWidth / 2;
    }
    if(viewHeight < screenHeight) {
        y = screenHeight / 2 ;
    }

    self.scrollView.contentInset = UIEdgeInsetsMake(y, x, y, x);
}

Gwarantuje to, że gdy obraz jest mniejszy niż ekran, wokół niego jest wystarczająco dużo miejsca, dzięki czemu można go ustawić dokładnie w wybranym miejscu.

(zakładając, że Twój UIScrollView zawiera UIImageView do przechowywania obrazu)

Zasadniczo chodzi o to, aby sprawdzić, czy szerokość / wysokość widoku obrazu jest mniejsza niż szerokość / wysokość ekranu, a jeśli tak, utwórz wstawkę o połowę szerokości / wysokości ekranu (prawdopodobnie możesz to powiększyć, jeśli chcesz, aby obraz był wyjść poza granice ekranu).

Zauważ, że ponieważ jest to UIScrollViewDelegate metoda , nie zapomnij dodać jej do deklaracji kontrolera widoku, aby uniknąć problemu z kompilacją.

Vicarius
źródło
7

Firma Apple udostępniła nagrania z sesji WWDC 2010 wszystkim członkom programu dla programistów iPhone'a. Jednym z omawianych tematów jest sposób, w jaki stworzyli aplikację do zdjęć !!! Tworzą bardzo podobną aplikację krok po kroku i udostępniają cały kod za darmo.

Nie używa też prywatnego interfejsu API. Nie mogę umieścić tutaj żadnego kodu z powodu umowy o zachowaniu poufności, ale tutaj jest link do przykładowego kodu do pobrania. Prawdopodobnie będziesz musiał się zalogować, aby uzyskać dostęp.

http://connect.apple.com/cgi-bin/WebObjects/MemberSite.woa/wa/getSoftware?code=y&source=x&bundleID=20645

A oto łącze do strony iTunes WWDC:

http://insideapple.apple.com/redir/cbx-cgi.do?v=2&la=en&lc=&a=kGSol9sgPHP%2BtlWtLp%2BEP%2FnxnZarjWJglPBZRHd3oDbACudP51JNGS8KlsFgxZto9X%2BTsnqSbeUSWX0doe%2Fzv%2FN5XV55%2FomsyfRgFBysOnIVggO%2Fn2p%2BiweDK%2F%2FmsIXj

Jonasza
źródło
1
Przykładem, o którym mowa, jest MyImagePicker i, co zabawne, wykazuje ten sam problem.
Colin Barrett
1
Powinienem być bardziej dokładny. Przykładem, o którym mowa, jest w rzeczywistości „PhotoScroller”, a nie „MyImagePicker”. Masz rację, że „MyImagePicker” nie działa poprawnie. Ale „PhotoScroller” tak. Spróbuj.
Jonah
Czy pamiętasz może tytuł filmu WWDC, w którym omawiają Photoscroller?
Grzegorz Adam Hankiewicz
1
Nazywa się „Projektowanie aplikacji z widokami przewijania”.
Jonah
2

Ok, to rozwiązanie działa dla mnie. Mam podklasę UIScrollViewz odniesieniem do tego, UIImageViewktóry jest wyświetlany. Przy każdym UIScrollViewpowiększeniu właściwość contentSize jest dostosowywana. To w seterze odpowiednio wyskaluję UIImageViewi dostosowuję jego położenie środkowe.

-(void) setContentSize:(CGSize) size{
CGSize lSelfSize = self.frame.size;
CGPoint mid;
if(self.zoomScale >= self.minimumZoomScale){
    CGSize lImageSize = cachedImageView.initialSize;
    float newHeight = lImageSize.height * self.zoomScale;

    if (newHeight < lSelfSize.height ) {
        newHeight = lSelfSize.height;
    }
    size.height = newHeight;

    float newWidth = lImageSize.width * self.zoomScale;
    if (newWidth < lSelfSize.width ) {
        newWidth = lSelfSize.width;
    }
    size.width = newWidth;
    mid = CGPointMake(size.width/2, size.height/2);

}
else {
    mid = CGPointMake(lSelfSize.width/2, lSelfSize.height/2);
}

cachedImageView.center = mid;
[super  setContentSize:size];
[self printLocations];
NSLog(@"zoom %f setting size %f x %f",self.zoomScale,size.width,size.height);
}

Zawsze ustawiam obraz na zmieniam jego UIScrollViewrozmiar. W UIScrollViewwidoku przewijania jest również utworzona przeze mnie niestandardowa klasa.

-(void) resetSize{
    if (!scrollView){//scroll view is view containing imageview
        return;
    }

    CGSize lSize = scrollView.frame.size;

    CGSize lSelfSize = self.image.size; 
    float lWidth = lSize.width/lSelfSize.width;
    float lHeight = lSize.height/lSelfSize.height;

    // choose minimum scale so image width fits screen
    float factor  = (lWidth<lHeight)?lWidth:lHeight;

    initialSize.height = lSelfSize.height  * factor;
    initialSize.width = lSelfSize.width  * factor;

    [scrollView setContentSize:lSize];
    [scrollView setContentOffset:CGPointZero];
    scrollView.userInteractionEnabled = YES;
}

Dzięki tym dwóm metodom mogę uzyskać widok, który zachowuje się tak jak aplikacja do zdjęć.

AHA
źródło
Wydaje się, że to załatwia sprawę i jest to dość proste rozwiązanie. Niektóre przejścia zoomu nie są idealne, ale uważam, że można to naprawić. Jeszcze trochę poeksperymentuję.
hpique
Rozwiązanie było jasne przed aktualizacją. Teraz to trochę zagmatwane. Możesz wyjaśnić?
hpique
2

Sposób, w jaki to zrobiłem, polega na dodaniu dodatkowego widoku do hierarchii:

UIScrollView -> UIView -> UIImageView

Podaj UIViewten sam współczynnik proporcji co twój UIScrollViewi wyśrodkuj UIImageViewna tym.

hatfinch
źródło
Dzięki, szczygło. Spróbuję tego również. Czy możesz opublikować przykładowy kod lub zmienić mój przykładowy kod, aby pokazać, w jaki sposób tworzysz hierarchię widoków?
hpique
Tego rodzaju prace. Z wyjątkiem faktu, że ponieważ UIView ma rozmiar UIScrollView, jeśli obraz jest mniejszy (tj. Poziomo zamiast pionowego), można przewijać część obrazu poza ekran. Aplikacja do zdjęć nie pozwala na to i wygląda o wiele ładniej.
Jonah
2

Wiem, że niektóre odpowiedzi powyżej są poprawne, ale chcę tylko udzielić odpowiedzi z pewnym wyjaśnieniem, komentarze pomogą Ci zrozumieć, dlaczego to lubimy.

Kiedy ładuję scrollView po raz pierwszy, piszę następujący kod, aby go wyśrodkować, proszę zauważyć, że ustawiliśmy contentOffsetnajpierw, a następniecontentInset

    scrollView.maximumZoomScale = 8
    scrollView.minimumZoomScale = 1

    // set vContent frame
    vContent.frame = CGRect(x: 0,
                            y: 0  ,
                            width: vContentWidth,
                            height: vContentWidth)
    // set scrollView.contentSize
    scrollView.contentSize = vContent.frame.size

    //on the X direction, if contentSize.width > scrollView.bounds.with, move scrollView from 0 to offsetX to make it center(using `scrollView.contentOffset`)
    // if not, don't need to set offset, but we need to set contentInset to make it center.(using `scrollView.contentInset`)
    // so does the Y direction.
    let offsetX = max((scrollView.contentSize.width - scrollView.bounds.width) * 0.5, 0)
    let offsetY = max((scrollView.contentSize.height - scrollView.bounds.height) * 0.5, 0)
    scrollView.contentOffset = CGPoint(x: offsetX, y: offsetY)

    let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)

Następnie, gdy szczypię vContent, piszę następujący kod, aby go wyśrodkować.

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    //we just need to ensure that the content is in the center when the contentSize is less than scrollView.size.
    let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)
}
Changwei
źródło
2

Jeśli contentInsetnie jest potrzebny do niczego innego, można go użyć do wyśrodkowania zawartości widoku przewijania.

class ContentCenteringScrollView: UIScrollView {

    override var bounds: CGRect {
        didSet { updateContentInset() }
    }

    override var contentSize: CGSize {
        didSet { updateContentInset() }
    }

    private func updateContentInset() {
        var top = CGFloat(0)
        var left = CGFloat(0)
        if contentSize.width < bounds.width {
            left = (bounds.width - contentSize.width) / 2
        }
        if contentSize.height < bounds.height {
            top = (bounds.height - contentSize.height) / 2
        }
        contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
    }
}

Zaletą tego podejścia jest to, że nadal można używać contentLayoutGuidedo umieszczania treści w widoku scrollview

scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    imageView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
    imageView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
    imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
    imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])

lub po prostu przeciągnij i upuść zawartość w Interface Builder Xcode.

Wojciech Nagrodzki
źródło
1

Możesz obserwować contentSizewłaściwość UIScrollView (używając obserwacji klucz-wartość lub podobnego) i automatycznie dostosowywać, contentInsetgdy contentSizezmiany są mniejsze niż rozmiar widoku przewijania.

Tim
źródło
Spróbuję. Czy można to zrobić z metodami UIScrollViewDelegate zamiast obserwować contentSize?
hpique
Najprawdopodobniej; użyjesz - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale(opisanego na developer.apple.com/iphone/library/documentation/UIKit/… ), ale wolałbym obserwować, contentSizeponieważ w przeciwnym razie i tak skończysz odrzucając nową skalę powiększenia i znajdując ją w widoku. (Chyba że potrafisz wyciągnąć niesamowitą matematykę na podstawie względnych rozmiarów twoich widoków.)
Tim
Dzięki, Tim. scrollViewDidEndZooming jest tym, czego używam. Zobacz omawiany kod (dodałem go po Twojej pierwszej odpowiedzi). To prawie działa. Jedyny problem, który pozostaje, jeśli uszczypnę obraz po osiągnięciu minimalnej skali ZoomScale, wróci on do lewego górnego rogu.
hpique
Czy masz na myśli to, że działa (wyśrodkowuje obraz) w celu uszczypnięcia z dowolnej skali innej niż minimalna skala powiększenia i zepsuje się tylko wtedy, gdy spróbujesz ponownie go uszczypnąć, gdy osiągniesz minimalną skalę? A może w ogóle nie działa przy szczypaniu do minimalnej skali?
Tim
Pierwszy. Wyśrodkowuje obraz w celu uszczypnięcia z dowolnej skali innej niż minimalna skala powiększenia i zepsuje się, jeśli spróbujesz go ponownie uszczypnąć, gdy osiągniesz minimalną skalę. Ponadto, gdy osiągnie minimalną skalę powiększenia za pierwszym razem, zawartość jest przez chwilę animowana, jakby pochodziła z góry, co powoduje krótki dziwny efekt. Dzieje się tak przynajmniej na symulatorze 3.0.
hpique
1

Jednym z eleganckich sposobów na wyśrodkowanie treści UISCrollViewjest to.

Dodaj jednego obserwatora do contentSize swojego UIScrollView, więc ta metoda będzie wywoływana za każdym razem, gdy zmieni się zawartość ...

[myScrollView addObserver:delegate 
               forKeyPath:@"contentSize"
                  options:(NSKeyValueObservingOptionNew) 
                  context:NULL];

Teraz twoja metoda obserwatora:

- (void)observeValueForKeyPath:(NSString *)keyPath   ofObject:(id)object   change:(NSDictionary *)change   context:(void *)context { 

    // Correct Object Class.
    UIScrollView *pointer = object;

    // Calculate Center.
    CGFloat topCorrect = ([pointer bounds].size.height - [pointer viewWithTag:100].bounds.size.height * [pointer zoomScale])  / 2.0 ;
            topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );

    topCorrect = topCorrect - (  pointer.frame.origin.y - imageGallery.frame.origin.y );

    // Apply Correct Center.
    pointer.center = CGPointMake(pointer.center.x,
                                 pointer.center.y + topCorrect ); }
  • Powinieneś zmienić [pointer viewWithTag:100]. Zastąp widokiem zawartości UIView.

    • Zmień także imageGallerywskazanie na rozmiar okna.

To poprawi środek treści za każdym razem, gdy zmieni się jego rozmiar.

UWAGA: Jedynym sposobem, w jaki ta zawartość nie działa zbyt dobrze, jest standardowa funkcja powiększania w UIScrollView.

Zespół deweloperski SEQOY
źródło
Nie mogłem tego zrobić. Wygląda na to, że wyśrodkowujesz scrollView, a nie jego zawartość. Dlaczego rozmiar okna ma znaczenie? Czy kod nie powinien również korygować pozycji x?
hpique
1

To jest moje rozwiązanie tego problemu, które działa całkiem nieźle w przypadku każdego widoku w przewijanym widoku.

-(void)scrollViewDidZoom:(__unused UIScrollView *)scrollView 
    {
    CGFloat top;
    CGFloat left;
    CGFloat bottom;
    CGFloat right;

    if (_scrollView.contentSize.width < scrollView.bounds.size.width) {
        DDLogInfo(@"contentSize %@",NSStringFromCGSize(_scrollView.contentSize));

        CGFloat width = (_scrollView.bounds.size.width-_scrollView.contentSize.width)/2.0;

        left = width;
        right = width;


    }else {
        left = kInset;
        right = kInset;
    }

    if (_scrollView.contentSize.height < scrollView.bounds.size.height) {

        CGFloat height = (_scrollView.bounds.size.height-_scrollView.contentSize.height)/2.0;

        top = height;
        bottom = height;

    }else {
        top = kInset;
        right = kInset;
    }

    _scrollView.contentInset = UIEdgeInsetsMake(top, left, bottom, right);



  if ([self.tiledScrollViewDelegate respondsToSelector:@selector(tiledScrollViewDidZoom:)])
  {
        [self.tiledScrollViewDelegate tiledScrollViewDidZoom:self];
  }
}
beat843796
źródło
1

Jest tu mnóstwo rozwiązań, ale zaryzykowałbym umieszczenie tutaj własnego. Jest to dobre z dwóch powodów: nie psuje funkcji powiększania, podobnie jak aktualizacja ramki widoku obrazu w toku, a także szanuje oryginalne wstawki widoku przewijania (powiedzmy, zdefiniowane w xib lub storyboardie w celu wdzięcznej obsługi półprzezroczystych pasków narzędzi itp.) .

Najpierw zdefiniuj małego pomocnika:

CGSize CGSizeWithAspectFit(CGSize containerSize, CGSize contentSize) {
    CGFloat containerAspect = containerSize.width / containerSize.height,
            contentAspect = contentSize.width / contentSize.height;

    CGFloat scale = containerAspect > contentAspect
                    ? containerSize.height / contentSize.height
                    : containerSize.width / contentSize.width;

    return CGSizeMake(contentSize.width * scale, contentSize.height * scale);
}

Aby zachować oryginalne wstawki, zdefiniowane pole:

UIEdgeInsets originalScrollViewInsets;

I gdzieś w widoku DidLoad wypełnij go:

originalScrollViewInsets = self.scrollView.contentInset;

Aby umieścić UIImageView w UIScrollView (zakładając, że sam UIImage jest w loadImage var):

CGSize containerSize = self.scrollView.bounds.size;
containerSize.height -= originalScrollViewInsets.top + originalScrollViewInsets.bottom;
containerSize.width -= originalScrollViewInsets.left + originalScrollViewInsets.right;

CGSize contentSize = CGSizeWithAspectFit(containerSize, loadedImage.size);

UIImageView *imageView = [[UIImageView alloc] initWithFrame:(CGRect) { CGPointZero, contentSize }];
imageView.autoresizingMask = UIViewAutoresizingNone;
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = loadedImage;

[self.scrollView addSubview:imageView];
self.scrollView.contentSize = contentSize;

[self centerImageViewInScrollView];

scrollViewDidZoom: from UIScrollViewDelegate dla tego widoku przewijania:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    if (scrollView == self.scrollView) {
        [self centerImageViewInScrollView];
    }
}

Wreszcie, skupiając się:

- (void)centerImageViewInScrollView {
    CGFloat excessiveWidth = MAX(0.0, self.scrollView.bounds.size.width - self.scrollView.contentSize.width),
            excessiveHeight = MAX(0.0, self.scrollView.bounds.size.height - self.scrollView.contentSize.height),
            insetX = excessiveWidth / 2.0,
            insetY = excessiveHeight / 2.0;

    self.scrollView.contentInset = UIEdgeInsetsMake(
            MAX(insetY, originalScrollViewInsets.top),
            MAX(insetX, originalScrollViewInsets.left),
            MAX(insetY, originalScrollViewInsets.bottom),
            MAX(insetX, originalScrollViewInsets.right)
    );
}

Nie testowałem jeszcze zmiany orientacji (tj. Poprawnej reakcji na zmianę rozmiaru samego UIScrollView), ale naprawienie tego powinno być stosunkowo łatwe.

jazzcat
źródło
1

Przekonasz się, że rozwiązanie opublikowane przez Erdemusa działa, ale… Istnieją przypadki, w których metoda scrollViewDidZoom nie jest wywoływana i obraz jest zablokowany w lewym górnym rogu. Prostym rozwiązaniem jest jawne wywołanie metody podczas początkowego wyświetlania obrazu, na przykład:

[self scrollViewDidZoom: scrollView];

W wielu przypadkach możesz wywoływać tę metodę dwukrotnie, ale jest to czystsze rozwiązanie niż niektóre inne odpowiedzi w tym temacie.

Russes
źródło
1

Przykład Apple Photo Scroller robi dokładnie to, czego szukasz. Umieść to w swojej podklasie UIScrollView i zmień _zoomView na UIImageView.

-(void)layoutSubviews{
  [super layoutSubviews];
  // center the zoom view as it becomes smaller than the size of the screen
  CGSize boundsSize = self.bounds.size;
  CGRect frameToCenter = self.imageView.frame;
  // center horizontally
  if (frameToCenter.size.width < boundsSize.width){
     frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
  }else{
    frameToCenter.origin.x = 0;
  }
  // center vertically
  if (frameToCenter.size.height < boundsSize.height){
     frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
  }else{
    frameToCenter.origin.y = 0;
  }
  self.imageView.frame = frameToCenter; 
}

Przykładowy kod Apple Photo Scroller

Korey Hinton
źródło
1

Aby animacja przebiegała przyjemnie, ustaw

self.scrollview.bouncesZoom = NO;

i skorzystaj z tej funkcji (znajdując środek używając metody podanej w tej odpowiedzi )

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
    [UIView animateWithDuration:0.2 animations:^{
        float offsetX = MAX((scrollView.bounds.size.width-scrollView.contentSize.width)/2, 0);
        float offsetY = MAX((scrollView.bounds.size.height-scrollView.contentSize.height)/2, 0);
        self.imageCoverView.center = CGPointMake(scrollView.contentSize.width*0.5+offsetX, scrollView.contentSize.height*0.5+offsetY);
    }];
}

Tworzy to efekt odbijania się, ale wcześniej nie wymaga żadnych gwałtownych ruchów.

ldanilek
źródło
1

Tylko zatwierdzona odpowiedź szybko, ale bez tworzenia podklas przy użyciu delegata

func centerScrollViewContents(scrollView: UIScrollView) {
    let contentSize = scrollView.contentSize
    let scrollViewSize = scrollView.frame.size;
    var contentOffset = scrollView.contentOffset;

    if (contentSize.width < scrollViewSize.width) {
        contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0
    }

    if (contentSize.height < scrollViewSize.height) {
        contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0
    }

    scrollView.setContentOffset(contentOffset, animated: false)
}

// UIScrollViewDelegate    
func scrollViewDidZoom(scrollView: UIScrollView) {
    centerScrollViewContents(scrollView)
}
LightMan
źródło
1

W przypadku, gdy twój wewnętrzny imageView ma początkową określoną szerokość (np. 300) i chcesz po prostu wyśrodkować jego szerokość tylko przy powiększeniu mniejszym niż jego początkowa szerokość, może to również pomóc.

 func scrollViewDidZoom(scrollView: UIScrollView){
    if imageView.frame.size.width < 300{
        imageView.center.x = self.view.frame.width/2
    }
  }
dejix
źródło
0

Oto obecny sposób, w jaki wykonuję tę pracę. Jest lepiej, ale nadal nie jest doskonały. Spróbuj ustawić:

 myScrollView.bouncesZoom = YES; 

aby rozwiązać problem polegający na tym, że widok nie jest wyśrodkowany, gdy w minZoomScale.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGSize screenSize = [[self view] bounds].size;//[[UIScreen mainScreen] bounds].size;//
CGSize photoSize = [yourImage size];
CGFloat topInset = (screenSize.height - photoSize.height * [myScrollView zoomScale]) / 2.0;
CGFloat sideInset = (screenSize.width - photoSize.width * [myScrollView zoomScale]) / 2.0;

if (topInset < 0.0)
{ topInset = 0.0; }
if (sideInset < 0.0)
{ sideInset = 0.0; } 
[myScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
ApplicationDelegate *appDelegate = (ApplicationDelegate *)[[UIApplication sharedApplication] delegate];

CGFloat scrollViewHeight; //Used later to calculate the height of the scrollView
if (appDelegate.navigationController.navigationBar.hidden == YES) //If the NavBar is Hidden, set scrollViewHeight to 480
{ scrollViewHeight = 480; }
if (appDelegate.navigationController.navigationBar.hidden == NO) //If the NavBar not Hidden, set scrollViewHeight to 360
{ scrollViewHeight = 368; }

imageView.frame = CGRectMake(0, 0, CGImageGetWidth(yourImage)* [myScrollView zoomScale], CGImageGetHeight(yourImage)* [myScrollView zoomScale]);

[imageView setContentMode:UIViewContentModeCenter];
}

Wykonuję również następujące czynności, aby zapobiec przyklejaniu się obrazu z boku po pomniejszeniu.

- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
myScrollView.frame = CGRectMake(0, 0, 320, 420);
 //put the correct parameters for your scroll view width and height above
}
Jonasza
źródło
Cześć Jonah. Wygląda na to, że od jakiegoś czasu mamy do czynienia z tym samym problemem. Sprawdzę twoje dwa rozwiązania i wkrótce odpowie.
hpique
Cześć Jonah, wypróbowałem twoje najnowsze rozwiązanie. Jednak czym jest tymczasowy obraz? Próbowałem umieścić tymczasowyImage = imageView.image; jednak po powiększeniu obraz znika. Dzięki, Pannag
psanketi
Pannag, tymczasowy Obraz został źle nazwany. Powinien nazywać się myImage, ponieważ jest to dowolny obraz, którego używasz. Przepraszam za zamieszanie.
Jonasz
0

OK, myślę, że znalazłem całkiem dobre rozwiązanie tego problemu. Sztuczka polega na tym, aby stale regulować imageView'sramę. Uważam, że działa to znacznie lepiej niż ciągłe dostosowywanie contentInsetslubcontentOffSets . Musiałem dodać trochę dodatkowego kodu, aby pomieścić zarówno obrazy pionowe, jak i poziome.

Oto kod:

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

CGSize screenSize = [[self view] bounds].size;

if (myScrollView.zoomScale <= initialZoom +0.01) //This resolves a problem with the code not working correctly when zooming all the way out.
{
    imageView.frame = [[self view] bounds];
    [myScrollView setZoomScale:myScrollView.zoomScale +0.01];
}

if (myScrollView.zoomScale > initialZoom)
{
    if (CGImageGetWidth(temporaryImage.CGImage) > CGImageGetHeight(temporaryImage.CGImage)) //If the image is wider than tall, do the following...
    {
        if (screenSize.height >= CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is greater than the zoomed height of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), 368);
        }
        if (screenSize.height < CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is less than the zoomed height of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]);
        }
    }
    if (CGImageGetWidth(temporaryImage.CGImage) < CGImageGetHeight(temporaryImage.CGImage)) //If the image is taller than wide, do the following...
    {
        CGFloat portraitHeight;
        if (CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale] < 368)
        { portraitHeight = 368;}
        else {portraitHeight = CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale];}

        if (screenSize.width >= CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is greater than the zoomed width of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, 320, portraitHeight);
        }
        if (screenSize.width < CGImageGetWidth (temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is less than the zoomed width of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale], portraitHeight);
        }
    }
    [myScrollView setZoomScale:myScrollView.zoomScale -0.01];
}
Jonasza
źródło
0

Po prostu wyłącz paginację, aby działało dobrze:

scrollview.pagingEnabled = NO;
Nagaraj
źródło
0

Miałem dokładnie ten sam problem. Oto jak rozwiązałem

Ten kod powinien zostać wywołany jako wynik scrollView:DidScroll:

CGFloat imageHeight = self.imageView.frame.size.width * self.imageView.image.size.height / self.imageView.image.size.width;
BOOL imageSmallerThanContent = (imageHeight < self.scrollview.frame.size.height) ? YES : NO;
CGFloat topOffset = (self.imageView.frame.size.height - imageHeight) / 2;

// If image is not large enough setup content offset in a way that image is centered and not vertically scrollable
if (imageSmallerThanContent) {
     topOffset = topOffset - ((self.scrollview.frame.size.height - imageHeight)/2);
}

self.scrollview.contentInset = UIEdgeInsetsMake(topOffset * -1, 0, topOffset * -1, 0);
aryaxt
źródło
0

Chociaż pytanie jest trochę stare, problem nadal istnieje. Rozwiązałem to w Xcode 7 , wprowadzając ograniczenie przestrzeni pionowej najwyższego elementu (w tym przypadku topLabel) do superViews (the scrollView) top an, IBOutleta następnie przeliczając jego stałą za każdym razem, gdy zawartość zmienia się w zależności od wysokości podwidoków scrollView ( topLabeli bottomLabel).

class MyViewController: UIViewController {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var topLabel: UILabel!
    @IBOutlet weak var bottomLabel: UILabel!
    @IBOutlet weak var toTopConstraint: NSLayoutConstraint!

    override func viewDidLayoutSubviews() {
        let heightOfScrollViewContents = (topLabel.frame.origin.y + topLabel.frame.size.height - bottomLabel.frame.origin.y)
        // In my case abs() delivers the perfect result, but you could also check if the heightOfScrollViewContents is greater than 0.
        toTopConstraint.constant = abs((scrollView.frame.height - heightOfScrollViewContents) / 2)
    }

    func refreshContents() {
        // Set the label's text …

        self.view.layoutIfNeeded()
    }
}
Nick Podratz
źródło