UIScrollView paging horizontal seperti tab Mobile Safari

88

Mobile Safari memungkinkan Anda untuk berpindah halaman dengan memasukkan tampilan halaman horizontal UIScrollView dengan kontrol halaman di bagian bawah.

Saya mencoba meniru perilaku khusus ini di mana UIScrollView yang dapat digulir secara horizontal menunjukkan beberapa konten tampilan berikutnya.

Contoh yang diberikan Apple: PageControl menunjukkan cara menggunakan UIScrollView untuk paging horizontal, tetapi semua tampilan menggunakan lebar layar secara keseluruhan.

Bagaimana cara mendapatkan UIScrollView untuk menampilkan beberapa konten tampilan berikutnya seperti yang dilakukan Safari seluler?

firstresponder
sumber
2
Hanya pemikiran ... coba buat batas tampilan gulir lebih kecil dari layar, dan bermain-main dengan membuat tampilan ditampilkan dengan benar. (dan mengatur klip tampilan gulir ToBounds ke NO)
mjhoy
Saya ingin memiliki halaman yang lebih besar dari lebar uiscrollview (scroll horizontal). Dan pemikiran mjhoy benar-benar membantuku!
Sasho

Jawaban:

263

A UIScrollViewdengan paging diaktifkan akan berhenti di kelipatan lebar frame (atau tinggi). Jadi, langkah pertama adalah mencari tahu seberapa lebar halaman yang Anda inginkan. Buat itu menjadi lebar file UIScrollView. Kemudian, atur ukuran subview Anda betapapun besar yang Anda inginkan, dan atur pusatnya berdasarkan kelipatan UIScrollViewlebarnya.

Kemudian, karena Anda ingin melihat halaman lain, tentu saja, ditetapkan clipsToBoundsuntuk NOsebagai mhjoy dinyatakan. Bagian triknya sekarang adalah membuatnya menggulir ketika pengguna memulai tarik di luar rentang UIScrollViewbingkai. Solusi saya (ketika saya harus melakukan ini baru-baru ini) adalah sebagai berikut:

Buat UIViewsubclass (yaitu ClipView) yang akan berisi UIScrollViewdan subviews itu. Pada dasarnya, itu harus memiliki kerangka dari apa yang Anda anggap UIScrollViewakan terjadi dalam keadaan normal. Tempatkan UIScrollViewdi tengah ClipView. Pastikan ClipView's clipsToBoundsdisetel ke YESjika lebarnya kurang dari tampilan induknya. Juga, ClipViewkebutuhan referensi ke UIScrollView.

Langkah terakhir adalah mengganti - (UIView *)hitTest:withEvent:di dalam ClipView.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

Ini pada dasarnya memperluas area sentuh UIScrollViewke bingkai tampilan induknya, persis seperti yang Anda butuhkan.

Pilihan lain adalah melakukan subkelas UIScrollViewdan mengganti - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventmetodenya, namun Anda masih memerlukan tampilan kontainer untuk melakukan pemotongan, dan mungkin sulit untuk menentukan kapan harus kembali YEShanya berdasarkan UIScrollViewbingkai.

CATATAN: Anda juga harus melihat hitTest: withEvent: modifikasi Juri Pakaste jika Anda mengalami masalah dengan interaksi pengguna subview.

Ed Marty
sumber
3
Terima kasih Ed. Saya menggunakan UIScrollViewsubclass untuk lazy loading of views (in layoutSubviews), dan menggunakan a clippingRectuntuk melakukan pointInside:withEvent:pengujian. Bekerja dengan sangat baik, dan tidak diperlukan tampilan penampung tambahan.
ohhorob
1
Jawaban yang bagus. Saya menggunakan UIScrollViewsubclass seperti @ohhorob tapi dengan pandangan wadah untuk kliping dan kembali [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];di pointInside:withEvent:. Ini bekerja dengan sangat baik dan tidak ada masalah dengan interaksi pengguna yang disebutkan dalam jawaban @ JuriPakaste.
cduck
1
Wow, ini solusi yang sangat bagus. Hanya satu hal, pada pengaturan saya, halaman yang saya tambahkan memiliki UIButtons. Ketika saya mengganti metode hitTest, saya tidak mengizinkan saya mengetuk tombol-tombol itu. Saya dapat menggulir, tetapi tidak ada tindakan yang dapat dipilih di dalam halaman mana pun.
A Salcedo
8
Bagi mereka yang menemukan ini baru-baru ini sadar bahwa - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetditambahkan ke UIScrollViewDelegate di iOS5 berarti Anda tidak perlu melalui palaver ini lagi.
Mike Pollard
1
@MikePollard efeknya tidak senyap sama, targetContentOffset tidak cocok dengan situasi ini.
Kyle Fang
70

The ClipViewsolusi di atas bekerja untuk saya, tapi saya harus melakukan yang berbeda -[UIView hitTest:withEvent:]implementasi. Versi Ed Marty tidak mendapatkan interaksi pengguna yang bekerja dengan tampilan gulir vertikal yang saya miliki di dalam yang horizontal.

Versi berikut berfungsi untuk saya:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}
Juri Pakaste
sumber
Ini juga membantu mendapatkan tombol dan subview lainnya dengan interaksi pengguna berfungsi. :) terima kasih
Thomas Clayson
4
Terima kasih untuk kode ini, bagaimana saya dapat menggunakan modifikasi ini untuk mendapatkan kejadian dari subview (tombol) yang tidak ada dalam batas scrollview? Ini berfungsi jika sebuah tombol misalnya berada dalam batas-batas tampilan gulir pusat tetapi tidak di luar.
DreamOfMirrors
4
Ini sangat membantu. Ini harus dimasukkan sebagai modifikasi dari jawaban yang dipilih.
Pacu
2
Iya! Ini juga membuat interaksi pengguna dalam tampilan gulir berfungsi kembali! Saya memang harus mengatur userInteractionEnabled ke YES di ClipView agar ini berfungsi.
Tom van Zummeren
Saya harus mengulang melalui self.scrollView.subviews dan melakukan hitTest terlebih dahulu.
Bao Lei
8

Tetapkan ukuran bingkai scrollView sebagai ukuran halaman Anda:

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

Sekarang Anda dapat menggeser self.view, dan konten di scrollView akan digulir.
Juga gunakan scrollView.clipsToBounds = NO;untuk mencegah kliping konten.

Nikolay Tabunchenko
sumber
5

Saya akhirnya menggunakan UIScrollView kustom sendiri karena menurut saya itu adalah metode tercepat dan sederhana. Namun, saya tidak melihat kode persisnya, jadi saya pikir saya akan membagikannya. Kebutuhan saya adalah UIScrollView yang memiliki konten kecil dan oleh karena itu UIScrollView itu sendiri kecil untuk mencapai pengaruh paging. Karena pos menyatakan Anda tidak dapat menggeser. Tapi sekarang kamu bisa.

Buat kelas CustomScrollView dan subclass UIScrollView. Kemudian yang perlu Anda lakukan adalah menambahkan ini ke file .m:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

Ini memungkinkan Anda untuk menggulir dari sisi ke sisi (horizontal). Sesuaikan batas yang sesuai untuk mengatur area sentuh gesek / gulir Anda. Nikmati!

djneely
sumber
4

Saya telah membuat implementasi lain yang dapat mengembalikan tampilan gulir secara otomatis. Jadi tidak perlu memiliki IBOutlet yang akan membatasi penggunaan ulang dalam proyek.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}
alvinhu
sumber
1
Ini yang akan digunakan jika Anda sudah memiliki xib / storyboard. Jika tidak, saya tidak yakin bagaimana Anda akan mendapatkan referensi ke Paging / Scrollview. Ada ide?
mskw
3

Saya memiliki modifikasi lain yang berpotensi berguna untuk implementasi hitTest ClipView. Saya tidak suka harus memberikan referensi UIScrollView ke ClipView. Implementasi saya di bawah ini memungkinkan Anda untuk menggunakan kembali kelas ClipView untuk memperluas area uji-hit apa pun, dan tidak harus menyediakannya dengan referensi.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}
Rod Reddekopp
sumber
3

Saya menerapkan saran yang diberi suara positif di atas, tetapi UICollectionViewsaya menggunakan sesuatu yang dianggap keluar dari bingkai untuk keluar dari layar. Ini menyebabkan sel-sel di sekitar hanya merender di luar batas saat pengguna menggulir ke arahnya, yang tidak ideal.

Apa yang akhirnya saya lakukan adalah meniru perilaku scrollview dengan menambahkan metode di bawah ini ke delegasi (atau UICollectionViewLayout).

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

Ini untuk menghindari pendelegasian tindakan gesek seluruhnya, yang juga merupakan bonus. The UIScrollViewDelegatememiliki metode serupa yang disebut scrollViewWillEndDragging:withVelocity:targetContentOffset:yang dapat digunakan untuk UITableViews halaman dan UIScrollViews.

Kyle
sumber
2
Dave, saya berjuang untuk mencapai perilaku seperti itu menggunakan UICollectionView juga dan saya akhirnya menggunakan pendekatan yang sama yang Anda gambarkan di sini. Namun, pengalaman menggulir tidak sebaik tampilan gulir dengan paging diaktifkan. Ketika saya menggesek dengan kecepatan yang lebih besar, beberapa halaman digulir dan berhenti di halaman yang diusulkan. Yang ingin saya capai adalah menyimpan perilaku sebagai tampilan gulir normal dengan paging diaktifkan - kecepatan apa pun yang saya gunakan, saya hanya akan mendapatkan 1 halaman lagi. Apakah Anda tahu bagaimana melakukannya?
Peter Stajger
3

Aktifkan kejadian tap tembak pada tampilan anak dari tampilan gulir sambil mendukung teknik pertanyaan SO ini. Menggunakan referensi ke tampilan gulir (self.scrollView) agar mudah dibaca.

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

Tambahkan ini ke tampilan anak Anda untuk merekam acara sentuh:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(Ini adalah variasi dari solusi user1856273. Dibersihkan untuk keterbacaan dan memasukkan perbaikan bug Bartserk. Saya berpikir untuk mengedit jawaban user1856273 tetapi perubahan itu terlalu besar untuk dibuat.)

David James
sumber
Untuk mendapatkan ketukan untuk bekerja pada UIButton yang disematkan di subview, saya mengganti kode hitView = [childViews objectAtIndex: i]; dengan // temukan UIButton untuk (UIView * paneView di [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

versi saya Menekan tombol yang ada di gulungan - kerja =)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

tetapi hanya [[self subviews] objectAtIndex: 0] yang harus berupa scroll

kolbasek
sumber
Ini memecahkan masalah saya :) Perhatikan bahwa Anda tidak menambahkan scroll offset ke point.x saat Anda memeriksa batas, dan ini membuat kode tidak berfungsi dengan baik pada scroll horizontal. Setelah menambahkan itu, itu bekerja dengan baik!
Bartserk
2

Berikut adalah jawaban Swift berdasarkan jawaban Ed Marty tetapi juga termasuk modifikasi oleh Juri Pakaste untuk memungkinkan ketukan tombol dll di dalam scrollview.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Ben Packard
sumber
Terima kasih telah memperbarui ini :)
Liam Bolling