3d轮播 ios

2019-08-05 14:13:42 TanJiaXiang 阅读数 2239
(JXBanner 支持多种动画变换, 支持纯代码布局和Xib布局)

JXBanner依赖于JXPageControl,并包含许多自定义接口,如转换动画、视图结构和设置


    1. 开发环境: Xcode 7
    1. 运行条件: iOS(8.0+)
  • 开源框架:github地址

 

(如果使用有什么问题,可以留言,欢迎一起学习,欢迎star)


Installation [安装]

安装,只需将以下面代码添加到您的Podfile:


platform :ios, '8.0'

target 'TargetName' do
pod 'JXBanner'
end


UI效果

  • default

不需要设置JXBanner -> JXBannerLayoutParams

在这里插入图片描述


  • JXBannerTransformLinear

在这里插入图片描述


  • JXBannerTransformCoverflow

在这里插入图片描述


  • custom

需要实现JXBannerTransformable协议, 修改 UICollectionViewLayoutAttributes -> transform3D 或 transform 属性

在这里插入图片描述


Frame set [框架集合]

 

Banner 轮播图框架公用类文件
  • API —> 开发者可以调用的所有接口
  • Cell —> 框架提供cell基类 (如果想自定义cell内容, 可以新建cell继承于JXBannerBaseCell)
  • Common —> 框架公用类文件
  • Transform —> 动画效果类文件 ( 如果框架提供的动画效果不能满足开发者需求, 可以新建实现JXBannerTransformable协议的struct/class, 修改 UICollectionViewLayoutAttributes -> transform3D 或 transform 属性)
PageControl 指示器类文件
  • JXBannerPageControlBuilder —> pageControl的构建者类
  • JXBannerPageControlDefault —> 框架默认的pageControl样式 (可以通过实现JXBannerDataSource -> 【jxBanner(pageControl banner: numberOfPages: coverView: builder:) -> JXBannerPageControlBuilder】协议方法修改样式)

JXBanner 重要文件介绍

 

JXBannerParams 【banner 属性】
  • isAutoPlay —> 自动播放
  • isBounces —> 边界能否越界滑动
  • timeInterval —> 播放调度间隔
  • isShowPageControl —> 是否加载内部指示器(JXPageControl(框架特色)
  • cycleWay —> 轮播方式(框架特色) (forward:无线向右播放, skipEnd:首尾自定义动画跳转, rollingBack:左右回滚模式)
  • edgeTransitionType —> cycleWay 使用 skipEnd 中 可以选取动画方式
  • edgeTransitionSubtype —> cycleWay 使用 skipEnd 中 可以选取动画方式

JXBannerLayoutParams 【banner布局、动画属性】
  • itemSize —> cell大小。
  • itemSpacing —>cell左右边距。
  • layoutType —> 动画效果JXBannerTransformable(框架特色)
  • minimumScale —> cell 缩放系数。
  • minimumAlpha —> cell 透明度系数。
  • maximumAngle —> cell 旋转系数。
  • rateOfChange —> cell 变化系数。
  • rateHorisonMargin —> cell 水平间距调整系数。

JXBannerCellRegister 【cell注册构建者】
  • type —> 注册cell的类型,必须是JXBannerBaseCell的子类

  • reuseIdentifier —> cell重用标识

    var type: JXBannerBaseCell.Type
    var reuseIdentifier: String


JXBanner 使用

 

Example 1
  • 默认实现示例

 


import SnapKit
import JXBanner

class JXDefaultVC: UIViewController {
    
    var pageCount = 5
    
    lazy var banner: JXBanner = {
        let banner = JXBanner()
        banner.backgroundColor = UIColor.black
        banner.placeholderImgView.image = UIImage(named: "banner_placeholder")
        banner.delegate = self
        banner.dataSource = self
        return banner
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(banner)
        banner.snp.makeConstraints { (maker) in
            maker.left.right.equalTo(view)
            maker.height.equalTo(250)
            maker.top.equalTo(view.snp_top).offset(100)
        }
        self.automaticallyAdjustsScrollViewInsets = false
    }
    
    deinit {
        print("\(#function) ----------> \(#file.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? #file)")
    }
}

//MARK:- JXBannerDataSource
extension JXDefaultVC: JXBannerDataSource {
    
    // 注册重用Cell标识
    func jxBanner(_ banner: JXBannerType)
        -> (JXBannerCellRegister) {
            return JXBannerCellRegister(type: JXBannerCell.self,
                                        reuseIdentifier: "JXDefaultVCCell")
    }
    
    // 轮播总数
    func jxBanner(numberOfItems banner: JXBannerType)
        -> Int { return pageCount }
    
    // 轮播cell内容设置
    func jxBanner(_ banner: JXBannerType,
                  cellForItemAt index: Int,
                  cell: JXBannerBaseCell)
        -> JXBannerBaseCell {
            let tempCell: JXBannerCell = cell as! JXBannerCell
            tempCell.layer.cornerRadius = 8
            tempCell.layer.masksToBounds = true
            tempCell.imageView.image = UIImage(named: "banner_placeholder")
            tempCell.msgLabel.text = String(index) + "---来喽来喽,他真的来喽~"
            return tempCell
    }
    
    // banner基本设置(可选)
    func jxBanner(_ banner: JXBannerType,
                  layoutParams: JXBannerLayoutParams)
        -> JXBannerLayoutParams {
        return layoutParams
            .itemSize(CGSize(width: UIScreen.main.bounds.width - 40, height: 200))
            .itemSpacing(20)
    }
}

//MARK:- JXBannerDelegate
extension JXDefaultVC: JXBannerDelegate {
    
    // 点击cell回调
    public func jxBanner(_ banner: JXBannerType,
                         didSelectItemAt index: Int) {
        print(index)
    }
    
}



Example 2

 

  • 个性化设置

import SnapKit
import JXBanner
import JXPageControl

class JXCustomVC: UIViewController {
    
    var pageCount = 5
    
    lazy var linearBanner: JXBanner = {[weak self] in
        let banner = JXBanner()
        banner.placeholderImgView.image = UIImage(named: "banner_placeholder")
        banner.backgroundColor = UIColor.black
        banner.indentify = "linearBanner"
        banner.delegate = self
        banner.dataSource = self
        return banner
    }()
    
    lazy var converflowBanner: JXBanner = {
        let banner = JXBanner()
        banner.placeholderImgView.image = UIImage(named: "banner_placeholder")
        banner.backgroundColor = UIColor.black
        banner.indentify = "converflowBanner"
        banner.delegate = self
        banner.dataSource = self
        return banner
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(linearBanner)
        view.addSubview(converflowBanner)
        linearBanner.snp.makeConstraints {(maker) in
            maker.left.right.equalTo(view)
            maker.height.equalTo(200)
            maker.top.equalTo(view.snp_top).offset(100)
        }
        
        converflowBanner.snp.makeConstraints {(maker) in
            maker.left.right.height.equalTo(linearBanner)
            maker.top.equalTo(linearBanner.snp_bottom).offset(100)
        }
        
        self.automaticallyAdjustsScrollViewInsets = false
    }
    
    deinit {
        print("\(#function) ----------> \(#file.components(separatedBy: "/").last?.components(separatedBy: ".").first ?? #file)")
    }
}

//MARK:- JXBannerDataSource
extension JXCustomVC: JXBannerDataSource {
    
    // 注册重用Cell标识
    func jxBanner(_ banner: JXBannerType)
        -> (JXBannerCellRegister) {

            if banner.indentify == "linearBanner" {
                return JXBannerCellRegister(type: JXBannerCell.self,
                                            reuseIdentifier: "LinearBannerCell")
            }else {
                return JXBannerCellRegister(type: JXBannerCell.self,
                                            reuseIdentifier: "ConverflowBannerCell")
            }
    }
    
    // 轮播总数
    func jxBanner(numberOfItems banner: JXBannerType)
        -> Int { return pageCount }
    
    // 轮播cell内容设置
    func jxBanner(_ banner: JXBannerType,
                  cellForItemAt index: Int,
                  cell: JXBannerBaseCell)
        -> JXBannerBaseCell {
            let tempCell: JXBannerCell = cell as! JXBannerCell
            tempCell.layer.cornerRadius = 8
            tempCell.layer.masksToBounds = true
            tempCell.imageView.image = UIImage(named: "banner_placeholder")
            tempCell.msgLabel.text = String(index) + "---来喽来喽,他真的来喽~"
            return tempCell
    }
    
    // banner基本设置(可选)
    func jxBanner(_ banner: JXBannerType,
                  params: JXBannerParams)
        -> JXBannerParams {
        
            if banner.indentify == "linearBanner" {
                return params
                    .timeInterval(2)
                    .cycleWay(.forward)
            }else {
                return params
                    .timeInterval(3)
                    .cycleWay(.forward)
            }
    }
    
    // banner布局、动画设置
    func jxBanner(_ banner: JXBannerType,
                  layoutParams: JXBannerLayoutParams)
        -> JXBannerLayoutParams {
            
            if banner.indentify == "linearBanner" {
                return layoutParams
                    .layoutType(JXBannerTransformLinear())
                    .itemSize(CGSize(width: 250, height: 190))
                    .itemSpacing(10)
                    .rateOfChange(0.8)
                    .minimumScale(0.7)
                    .rateHorisonMargin(0.5)
                    .minimumAlpha(0.8)
            }else {
                return layoutParams
                    .layoutType(JXBannerTransformCoverflow())
                    .itemSize(CGSize(width: 300, height: 190))
                    .itemSpacing(0)
                    .maximumAngle(0.25)
                    .rateHorisonMargin(0.3)
                    .minimumAlpha(0.8)
            }
    }
    
    // 自定义pageControl样式、布局
    //(基于jxPageControl, 如果不适用JXPageControl, 设置isShowPageControl = false, 内部pageControl将不会再次加载 ) 
    func jxBanner(pageControl banner: JXBannerType,
                  numberOfPages: Int,
                  coverView: UIView,
                  builder: JXBannerPageControlBuilder) -> JXBannerPageControlBuilder {

        if banner.indentify == "linearBanner" {
            let pageControl = JXPageControlScale()
            pageControl.contentMode = .bottom
            pageControl.activeSize = CGSize(width: 15, height: 6)
            pageControl.inactiveSize = CGSize(width: 6, height: 6)
            pageControl.activeColor = UIColor.red
            pageControl.inactiveColor = UIColor.lightGray
            pageControl.columnSpacing = 0
            pageControl.isAnimation = true
            builder.pageControl = pageControl
            builder.layout = {
                pageControl.snp.makeConstraints { (maker) in
                    maker.left.right.equalTo(coverView)
                    maker.top.equalTo(coverView.snp_bottom).offset(10)
                    maker.height.equalTo(20)
                }
            }
            return builder

        }else {
            let pageControl = JXPageControlExchange()
            pageControl.contentMode = .bottom
            pageControl.activeSize = CGSize(width: 15, height: 6)
            pageControl.inactiveSize = CGSize(width: 6, height: 6)
            pageControl.activeColor = UIColor.red
            pageControl.inactiveColor = UIColor.lightGray
            pageControl.columnSpacing = 0
            builder.pageControl = pageControl
            builder.layout = {
                pageControl.snp.makeConstraints { (maker) in
                    maker.left.right.equalTo(coverView)
                    maker.top.equalTo(coverView.snp_bottom).offset(10)
                    maker.height.equalTo(20)
                }
            }
            return builder
        }

    }
    
}

//MARK:- JXBannerDelegate
extension JXCustomVC: JXBannerDelegate {
    
    // 点击cell回调
    public func jxBanner(_ banner: JXBannerType,
                         didSelectItemAt index: Int) {
        print(index)
    }
    
    // 设置自定义覆盖View, 比如添加自定义外部pageControl和布局
    func jxBanner(_ banner: JXBannerType, coverView: UIView) {
        let title = UILabel()
        title.frame = CGRect(x: 0, y: 0, width: 100, height: 30)
        title.text = "JXBanner"
        title.textColor = UIColor.red
        title.font = UIFont.systemFont(ofSize: 16)
        coverView.addSubview(title)
    }
    
    // 最中心显示cell 索引
    func jxBanner(_ banner: JXBannerType, center index: Int) {
        print(index)
    }
}

Example 3

如果框架提供的动画效果不能满足开发者需求:

    1. 轮播图动画样式开发者可以自定义实现, 只要是新建实现JXBannerTransformable协议的struct/class, 修改 UICollectionViewLayoutAttributes -> transform3D 或 transform 属性)

//
//  JXCustomTransform.swift
//  JXBanner_Example
//
//  Created by 谭家祥 on 2019/7/30.
//  Copyright © 2019 CocoaPods. All rights reserved.
//

import UIKit
import JXBanner

struct JXCustomTransform: JXBannerTransformable {
    
    public func transformToAttributes(collectionView: UICollectionView,
                                      params: JXBannerLayoutParams,
                                      attributes: UICollectionViewLayoutAttributes) {
        
        let collectionViewWidth = collectionView.frame.width
        if collectionViewWidth <= 0 { return }
        
        let centetX = collectionView.contentOffset.x + collectionViewWidth * 0.5;
        let delta = abs(attributes.center.x - centetX)
        let calculateRate = 1 - delta / collectionViewWidth
        let angle = min(delta / collectionViewWidth * (1 - params.rateOfChange), params.maximumAngle)
        let alpha = max(calculateRate, params.minimumAlpha)
        
        
        applyCoverflowTransformToAttributes(viewCentetX: centetX,
                                            attributes: attributes,
                                            params: params,
                                            angle: angle,
                                            alpha: alpha,
                                            calculateRate: calculateRate)
    }
    
    func applyCoverflowTransformToAttributes(viewCentetX: CGFloat,
                                             attributes: UICollectionViewLayoutAttributes,
                                             params: JXBannerLayoutParams,
                                             angle: CGFloat,
                                             alpha: CGFloat,
                                             calculateRate: CGFloat) -> Void {
        var transform3D: CATransform3D = CATransform3DIdentity
        
        
        let location = JXBannerTransfrom.itemLocation(viewCentetX: viewCentetX,
                                                      itemCenterX: attributes.center.x)

        var _angle = angle
        var _alpha = alpha
        var _translateX: CGFloat = 0
        var _translateY: CGFloat = 0
        attributes.zIndex = 0
        
        switch location {
        case .left:
            _angle = angle
            _translateX = 0.2 * attributes.size.width * (1 - calculateRate) / 4
            _translateY = 0.4 * attributes.size.height * (1 - calculateRate)
            
            
        case .right:
            _angle = -angle
            _translateX = -0.2 * attributes.size.width * (1 - calculateRate) / 4
            _translateY = 0.4 * attributes.size.height * (1 - calculateRate)
            
        case .center:
            _angle = 0
            _alpha = 1
            _translateY = 0
            attributes.zIndex = 10000
        }
        
        transform3D = CATransform3DTranslate(transform3D, _translateX, _translateY, 0)
        transform3D = CATransform3DRotate(transform3D, -CGFloat.pi * _angle, 0, 0, 1)
        attributes.alpha = _alpha
        attributes.transform3D = transform3D
    }

}


    1. 设置自定义实现动画

JXBannerDataSource -> 【jxBanner(_ banner: layoutParams: ) -> JXBannerLayoutParams】


    // JXCustomTransform()

    func jxBanner(_ banner: JXBannerType,
                  layoutParams: JXBannerLayoutParams)
        -> JXBannerLayoutParams {
            
            return layoutParams
                .layoutType(JXCustomTransform())
    }

更多设置可以参考示例 Demo地址

2016-06-04 17:22:24 iotjin 阅读数 721
原文

  http://www.cnblogs.com/colinhou/p/4594459.htm

转自:http://www.tuicool.com/articles/reu22eu


平时APP中的广告位或者滚动的新闻图片等用到的就是图片轮播这种效果,实现方式主要有两种,一种是ScrollView+ImageView,另一种则是通过CollectionView,今天总结的是ScrollView这种方式。

1.图片轮播效果实现

主要实现思路是:根据图片总数及宽高设置好ScrollView的大小,每切换一张图片相当于在ScrollView上进行一个图片宽度的移动行为,并加入定时器,实现自动轮播。

如图所示,设置好ScrollView及PageControl,ScrollView的contentSize根据图片数量确定,注意启用pagingEnabled这个属性,确保整页移动,同样pageControl也是根据图片数量来确定,每一页代表一张图片。


图片命名采用数字序号方式,方便使用,需要注意的是,pageControl是由0开始的,也就是0对应image1,1对应image2...依次类推

加载图片并将准备好的图片在ScrollView里设置好位置,即将这些图片一张紧挨着一张排列在ScrollView中。

通过ScrollView的代理方法,在ScrollView滚动结束的时候根据 contentOffset 更新页码。

定时器设置,这里设置为每隔2秒滚动更新一次,实际上就是每隔2秒更新一次页码,根据页码的变化,让ScrollView跟着移动,每次移动一张图片的距离

这里还需要注意的是,由于加入定时器有自动轮播的效果了,会与手动拖拽ScrollView冲突,即手动拖拽ScrollView过程时ScrollView可能自动移动更新图片了,显然这种效果是不符合用户习惯的,这时需要在ScrollView的代理事件中进行处理,即开始拖拽ScrollView时停止定时器,拖拽结束后再开启定时器。

那到这里是不是就结束了呢?我们看看效果图:

这里有两个问题:

(1)首先是移动到最后一张图片时无法移动了,如果是制作APP的新特性页面,这样的滚动效果已经可以了,但如果在广告位或者是滚动新闻这些场景下这种效果是不够好的,一般滚动到最后一张图片时,继续拖拽都会移动到第一张图片,实现一种滚动循环效果。

(2)定时器自动轮播图片时,确实图片循环轮播了,但是仔细看会发现,ScrollView是由最后一种图片位置生硬得拉回到第一张图片的位置,效果也不够理想。

解决办法下面接着说。

2.图片轮播无限循环效果实现

刚刚说到第一个问题,ScrollView移动到最后一张图片时无法移动了,这是因为ScrollView已经移动到最后,而图片又是依次排列,自然也就无法移动。

解决办法是,我们换一个思路实现图片轮播效果,ScrollView上只放三个ImageView,屏幕始终显示中间的ImageView,左边和右边的ImageView分别代表前一张图片和后一张图片,屏幕移动的时候,中间的ImageView变化,同时左右两边的ImageView也随之变化,两种边界情况:

(1)当屏幕显示最后一张图片时,右边的ImageView也即下一站图片应该是最开始的第一张图片;

(2)当屏幕显示最开始的第一张图片时,左边的ImageView也即上一张图片应该是最后一张图片。

这样三个ImageView不断变化就造成一种图片轮播无限循环的效果。 参考:http://www.cnblogs.com/kenshincui/p/3913885.html

      相对于之前的效果,有一些改变,主要有:

(1)ScrollView只需要设置三个ImageView即可,并且默认显示中间的ImageView

(2)根据ScrollView的移动情况,迅速变化三个ImageView中图片数据

(3)ImageView更新完毕后,偷偷把ScrollView拉回到中间的ImageView位置,这样视觉效果上就实现了无限循环的效果

效果图:

但是,这里在加入定时器后实现图片轮播自动循环时遇到了问题,主要是初始化显示第一张图片与根据定时器设置自动移动ScrollView有一些冲突,在code4App上找到其他人一个工程,采用的思路相同,单独封装了ScrollView进行处理,已经解决该问题。

可参考: http://code4app.com/ios/AdScrollerView/54955a78933bf0f2168b45b4

l
2016-04-11 08:04:40 wbwjx 阅读数 8065

概述

CoverFlow效果是IOS上自带的控件,类似3d轮播图效果。
在Android中也有很多的相关实现,大体都是使用Gallery和Camera来实现的。

相关开源库:ImageCoverFlow

效果图:

这里写图片描述

但是Gallery已经被标记过时,而且ViewPager更方便使用。而且ViewPager中有个接口ViewPagerTransformer,专门用于给ViewPager设置翻页动画的。

ViewPagerTransformer使用

自定义类实现ViewPagerTransformer接口,viewpager调用setPageTransformer即可。

//参数含义:page绘制顺序;transformer实例对象
ViewPager.setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer);

每次界面切换,transformPage()都会为viewpager容器下的缓存页面调用该方法。如,第三页可见且用户向第四页拖动,在操作的各个阶段为第二,三,四页分别调用(ViewPager默认缓存3页)。

//其中view即viewpager的当前item,
transformPage(View view, float position)

如果是3页的 情况下,当前也position的取值范围是[-1,1]
前面的position:[-Infinity,-1)
后面的position:(1,+Infinity]

clipChildren

clipChildren是指子控件是否超过padding区域,这个属性默认是true,利用此属性即可设置viewpager一屏展示多个item,如果将page 沿着y轴旋转即可实现类似coverflow效果了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:id="@+id/vp_container"
    android:layout_height="match_parent"
    android:clipChildren="false">

    <com.bobomee.android.scrollloopviewpager.autoscrollviewpager.AutoScrollViewPager
        android:id="@+id/picslooper3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="100dp"
        android:layout_marginRight="100dp"
        android:clipChildren="false" />

    <com.bobomee.android.drawableindicator.widget.DrawableIndicator
        android:id="@+id/pageIndexor3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        app:indicator_height="15dp"
        app:indicator_margin="15dp"
        app:indicator_select_src="@drawable/select_drawable"
        app:indicator_unselect_src="@drawable/unselect_drawable"
        app:indicator_width="15dp" />
</RelativeLayout>

java code:

  PagerAdapter adapter;
        final AutoScrollViewPager viewPager = (AutoScrollViewPager) findViewById(R.id.picslooper3);
        viewPager.setAdapter(adapter = new FragmentStateAdapter(getSupportFragmentManager()));
        viewPager.setDirection(AutoScrollViewPager.LEFT);
        viewPager.setPageTransformer(true, new RotateTransformer());

        BaseIndicator pageIndex = (BaseIndicator) findViewById(R.id.pageIndexor3);
        pageIndex.setViewPager(viewPager);

        viewPager.startAutoScroll();

        //设置幕后item的缓存数目
        viewPager.setOffscreenPageLimit(adapter.getCount());

        //设置页与页之间的间距
        int margin = ((ViewGroup.MarginLayoutParams) viewPager.getLayoutParams()).leftMargin;
        viewPager.setPageMargin(-(px2dip(this, margin)) / 2);

        //将父类的touch事件分发至viewPgaer,否则只能滑动中间的一个view对象
        RelativeLayout vpContainer = (RelativeLayout) findViewById(R.id.vp_container);
        vpContainer.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return viewPager.dispatchTouchEvent(event);
            }
        });

transformer:

public class RotateTransformer implements ViewPager.PageTransformer {

    public static final float MAX_SCALE = 1.2f;
    public static final float MIN_SCALE = 0.6f;

    @Override
    public void transformPage(View page, float position) {
        page.setRotationY(position * -45);

        if (position < -1) {
            position = -1;
        } else if (position > 1) {
            position = 1;
        }

        float tempScale = position < 0 ? 1 + position : 1 - position;

        float slope = (MAX_SCALE - MIN_SCALE) / 1;
        //一个公式
        float scaleValue = MIN_SCALE + tempScale * slope;
        page.setScaleX(scaleValue);
        page.setScaleY(scaleValue);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            page.getParent().requestLayout();
        }

    }
}

效果图:
3dbanner

实例代码:autoscrollloopviewpager

2018-07-24 14:16:40 u010960265 阅读数 23694

效果:
这里写图片描述

参考UITableView的UITableViewDataSource和UITableViewDelegate两个方法实现;支持五险轮播,可以加载本地图片,也可以加载网络图片,可以根据自己的需求自定义
Demo地址

UITableViewDelegate

/**
 *  当前显示cell的Size(中间页显示大小)
 *
 *  @param flowView <#flowView description#>
 *
 *  @return <#return value description#>
 */
- (CGSize)sizeForPageInFlowView:(HQFlowView *)flowView;

/**
 *  滚动到了某一列
 *
 *  @param pageNumber <#pageNumber description#>
 *  @param flowView   <#flowView description#>
 */
- (void)didScrollToPage:(NSInteger)pageNumber inFlowView:(HQFlowView *)flowView;

/**
 *  点击了第几个cell
 *
 *  @param subView 点击的控件
 *  @param subIndex    点击控件的index
 *
 *  @return <#return value description#>
 */
- (void)didSelectCell:(HQIndexBannerSubview *)subView withSubViewIndex:(NSInteger)subIndex;

UITableViewDataSource

/**
 *  返回显示View的个数
 *
 *  @param flowView <#flowView description#>
 *
 *  @return <#return value description#>
 */
- (NSInteger)numberOfPagesInFlowView:(HQFlowView *)flowView;

/**
 *  给某一列设置属性
 *
 *  @param flowView <#flowView description#>
 *  @param index    <#index description#>
 *
 *  @return <#return value description#>
 */
- (HQIndexBannerSubview *)flowView:(HQFlowView *)flowView cellForPageAtIndex:(NSInteger)index;
2016-08-16 11:14:49 Deft_MKJing 阅读数 8043

第二次更新:更加丰富的相册图片展示


1.由于不在继承与FlowLayout,只是单纯继承与UIColletionLayout

2.关键方法 layoutAttributesForItemAtIndexPath 在返回的对应index返回需要布局的UICollectionViewLayoutAttributes属性即可

3.还是直接下载代码看自定义的Layout吧,有问题请留言,大过年的写的比较仓促

传送门


个人感觉CollectionView过于强大,基本什么界面都能用他来完成需求,只是如果自定义Layout的时候可能性能开销大。如果是普通的需求,他和tableView并没有多大的区别,同样都是通过datasource和delegate两个代理来进行用户交互稍微回顾下CollectionView的构成

1.Cells

2.Supplementary Views 追加视图 (sectionHeader or footer)

3.Decoration Views 装饰视图 (用作背景展示) 这货貌似还没怎么用过

另一方面,对于cell的组织方式和样式,确实由于collectionView比tableView要复杂的多,因此没有按照tableView方式来定义,而是专门启用了一个布局类UICollectionViewlayout来对collectionView进行布局。废话不多说,系统自带的Layout已经是很常见了,今天写个自定义Layout来做些看起来还是蛮不错的Demo

正常人的Demo





先理论BB以下,不想看的请跳过

实现一个自定义Layout的正常做法就是继承与UICollectionViewlayout,然后重载以下方法,听我一一道来

三个必须手动重载的方法

1.-(CGSize)collectionViewContentSize

返回可见区域内的大小



2.-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

返回的是包含UICollectionViewlayoutAttributes对象的数组,该对象可以是cell,supplementary或装饰视图




返回对应indexPath下cell的布局属性

-(UICollectionViewLayoutAttributes _)layoutAttributesForItemAtIndexPath:(NSIndexPath _)indexPath



返回对应indexpath下对应的追加视图属性 (没有可不重载)


-(UICollectionViewLayoutAttributes _)layoutAttributesForSupplementaryViewOfKind:(NSString _)kind atIndexPath:(NSIndexPath *)indexPath


返回对应indexpath下对应的追加视图属性 (没有可不重载)


-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString_)decorationViewKind atIndexPath:(NSIndexPath _)indexPath


3.-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

当边界发生变化时,也就是滚动的时候,如果返回Yes,就是会一直调用,而且不断刷新重新计算布局信息
一个自动重载的方法

-(void)prepareLayout

一般在该方法中设定一些必要的layout的结构和初始需要的参数等。


注意:在需要更新layout时,需要给当前layout发送 -invalidateLayout,该消息会立即返回,并且预约在下一个loop的时候刷新当前layout,这一点和UIView的setNeedsLayout方法十分类似。在-invalidateLayout后的下一个collectionView的刷新loop中,又会从prepareLayout开始,依次再调用-collectionViewContentSize和-layoutAttributesForElementsInRect来生成更新后的布局。



Demo分析

1.简单的做个弹出的动画容器View 以下的self就是指弹出来的View,View上面给了一层underBackView,最终是在改View上面布局

// self是继承于UIView的,给上面的第一个View容器加个动画
- (void)showInSuperView:(UIView *)superView
{
    CAKeyframeAnimation *popAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
    popAnimation.duration = 0.25;
    popAnimation.values = @[[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1f, 0.1f, 1.0f)],
                            [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0f, 1.0f, 1.0f)]];
    popAnimation.keyTimes = @[@0.2f, @1.0f];
    popAnimation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                     [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                     [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
    // 在点击按钮的时候在改方法里面add到父视图上面去
    [superView addSubview:self];
    // 给View上面的容器View加动画
    [self.underBackView.layer addAnimation:popAnimation forKey:nil];
}


2.简单的懒加载布局视图以及一些回调delegate的设置就没必要多说了,大家都知道

#pragma mark - 懒加载
- (UIView *)underBackView
{
    if (_underBackView == nil) {
        _underBackView = [[UIView alloc] init];
        _underBackView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8];
        _underBackView.originX = 30;
        _underBackView.originY = 60;
        _underBackView.width = SCREEN_WIDTH - 2 * _underBackView.originX;
        _underBackView.height = SCREEN_HEIGHT - 2 * _underBackView.originY;
        _underBackView.layer.cornerRadius = 5;
        _underBackView.layer.borderColor = [UIColor redColor].CGColor;
        _underBackView.layer.borderWidth = 2.0f;
    }
    return _underBackView;
}

- (UILabel *)nameLabel
{
    if (_nameLabel == nil) {
        _nameLabel = [[UILabel alloc] init];
        _nameLabel.textAlignment = NSTextAlignmentCenter;
        _nameLabel.backgroundColor = [UIColor whiteColor];
        _nameLabel.font = [UIFont boldSystemFontOfSize:20];
        _nameLabel.textColor = [UIColor blueColor];
        _nameLabel.layer.cornerRadius = 5.0f;
        _nameLabel.layer.borderColor = [UIColor blackColor].CGColor;
        _nameLabel.layer.borderWidth = 2.0f;
    }
    return _nameLabel;
}

- (UIButton *)selectedButton
{
    if (_selectedButton == nil) {
        _selectedButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _selectedButton.backgroundColor = [UIColor blackColor];
        [_selectedButton setTitle:@"选这个" forState:UIControlStateNormal];
        [_selectedButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_selectedButton addTarget:self action:@selector(chooseDone:) forControlEvents:UIControlEventTouchUpInside];
        _selectedButton.layer.cornerRadius = 20.0f;
        _selectedButton.layer.borderWidth = 2.0f;
        _selectedButton.layer.borderColor = [UIColor whiteColor].CGColor;
    }
    return _selectedButton;
}
- (void)chooseDone:(UIButton *)button
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(selectedHero:)]) {
        [self.delegate selectedHero:self.dataSource[_selectedIndex]];
    }
}

- (UIButton *)closeButton
{
    if (_closeButton == nil) {
        _closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
        _closeButton.backgroundColor = [UIColor redColor];
        [_closeButton setImage:[UIImage imageNamed:@"close"] forState:UIControlStateNormal];
        [_closeButton addTarget:self action:@selector(close:) forControlEvents:UIControlEventTouchUpInside];
    }
    return _closeButton;
}

- (void)close:(UIButton *)button
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(closePopView)]) {
        [self.delegate closePopView];
    }
}
- (UICollectionView *)collectionView
{
    if (_collectionView == nil) {
        MKJCollectionViewFlowLayout *flow = [[MKJCollectionViewFlowLayout alloc] init];
        flow.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        flow.itemSize = CGSizeMake(self.underBackView.width / 2, self.underBackView.width - 100);
        flow.minimumLineSpacing = 30;
        flow.minimumInteritemSpacing = 30;
        flow.needAlpha = YES;
        flow.delegate = self;
        CGFloat oneX =self.underBackView.width / 4;
        flow.sectionInset = UIEdgeInsetsMake(0, oneX, 0, oneX);
        
        _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 30, self.underBackView.bounds.size.width, self.underBackView.bounds.size.height * 0.65) collectionViewLayout:flow];
        _collectionView.backgroundColor = [UIColor whiteColor];
        _collectionView.delegate = self;
        _collectionView.dataSource = self;
        _collectionView.showsHorizontalScrollIndicator = NO;
        [_collectionView registerNib:[UINib nibWithNibName:indentify bundle:nil] forCellWithReuseIdentifier:indentify];
    }
    return _collectionView;
}

3.关键是还是要看最终自定义的FlowLayout一些小东西的实现

重载的方法和方式在上面都已经理论了一番了,需要的自己去上面看看

第一个效果:需要一个放大的效果和透明的效果 

// 重载第一个方法
// 返回可见区域的的内容尺寸
- (CGSize)collectionViewContentSize
{
    return [super collectionViewContentSize];
}

// 重载方法第二个
// 返回rect中所有元素的布局属性
// 返回的是包含UICollectionViewLayoutAttributes的NSArray
// UICollectionViewAttributes可以是cell,追加视图以及装饰视图的信息,通过以下三个不同的方法可以获取到不同类型的UICollectionViewLayoutAttributes属性
// layoutAttributesForCellWithIndexPath:  返回对应cell的UICollectionViewAttributes布局属性
// layoutAtttibutesForSupplementaryViewOfKind:withIndexPath: 返回装饰的布局属性 如果没有追加视图可不重载
// layoutAttributesForDecorationViewOfKind:withIndexPath: 返回装饰的布局属性  如果没有可以不重载
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
    //1. 获取可见区域
    CGRect visibleRect = CGRectMake(self.collectionView.contentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    //2. 获得这个区域的item
    NSArray *visibleItemArray = [super layoutAttributesForElementsInRect:visibleRect];
    
    //3. 遍历,让靠近中心线的item方法,离开的缩小
    for (UICollectionViewLayoutAttributes *attributes in visibleItemArray)
    {
        //1. 获取每个item距离可见区域左侧边框的距离 有正负
        CGFloat leftMargin = attributes.center.x - self.collectionView.contentOffset.x;
        //2. 获取边框距离屏幕中心的距离(固定的)
        CGFloat halfCenterX = self.collectionView.frame.size.width / 2;
        //3. 获取距离中心的的偏移量,需要绝对值
        CGFloat absOffset = fabs(halfCenterX - leftMargin);
        //4. 获取的实际的缩放比例 距离中心越多,这个值就越小,也就是item的scale越小 中心是方法最大的
        CGFloat scale = 1 - absOffset / halfCenterX;
        //5. 缩放
        attributes.transform3D = CATransform3DMakeScale(1 + scale * MKJMinZoomScale, 1 + scale * MKJMinZoomScale, 1);
        
        
        // 是否需要透明
        if (self.needAlpha)
        {
            if (scale < 0.6)
            {
                attributes.alpha = 0.6;
            }
            else if (scale > 0.99)
            {
                attributes.alpha = 1.0;
            }
            else
            {
                attributes.alpha = scale;
            }
        }
    }
    NSArray *attributesArr = [[NSArray alloc] initWithArray:visibleItemArray copyItems:YES];
    return attributesArr;
}

第二个效果:如何让滚动的的item根据中心距离,自动居中对齐,避免滚动到哪停到哪

这里做了第一个和最后一个的判断,不然在头和尾会出现对不齐的情况

// 重载第四个属性,item自动中心对齐
// 该方法可写可不写,主要是让滚动的item根据距离中心的值,确定哪个必须展示在中心,不会像普通的那样滚动到哪里就停到哪里
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    // ProposeContentOffset是本来应该停下的位子
    // 1. 先给一个字段存储最小的偏移量 那么默认就是无限大
    CGFloat minOffset = CGFLOAT_MAX;
    // 2. 获取到可见区域的centerX
    CGFloat horizontalCenter = proposedContentOffset.x + self.collectionView.bounds.size.width / 2;
    // 3. 拿到可见区域的rect
    CGRect visibleRec = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    // 4. 获取到所有可见区域内的item数组
    NSArray *visibleAttributes = [super layoutAttributesForElementsInRect:visibleRec];
    
    // 遍历数组,找到距离中心最近偏移量是多少
    for (UICollectionViewLayoutAttributes *atts in visibleAttributes)
    {
        // 可见区域内每个item对应的中心X坐标
        CGFloat itemCenterX = atts.center.x;
        // 比较是否有更小的,有的话赋值给minOffset
        if (fabs(itemCenterX - horizontalCenter) <= fabs(minOffset)) {
            minOffset = itemCenterX - horizontalCenter;
        }
        
    }
    // 这里需要注意的是  上面获取到的minOffset有可能是负数,那么代表左边的item还没到中心,如果确定这种情况下左边的item是距离最近的,那么需要左边的item居中,意思就是collectionView的偏移量需要比原本更小才是,例如原先是1000的偏移,但是需要展示前一个item,所以需要1000减去某个偏移量,因此不需要更改偏移的正负
    
    // 但是当propose小于0的时候或者大于contentSize(除掉左侧和右侧偏移以及单个cell宽度)  、
    // 防止当第一个或者最后一个的时候不会有居中(偏移量超过了本身的宽度),直接卡在推荐的停留位置
    CGFloat centerOffsetX = proposedContentOffset.x + minOffset;
    if (centerOffsetX < 0) {
        centerOffsetX = 0;
    }
    
    if (centerOffsetX > self.collectionView.contentSize.width -(self.sectionInset.left + self.sectionInset.right + self.itemSize.width)) {
        centerOffsetX = floor(centerOffsetX);
    }
    return CGPointMake(centerOffsetX, proposedContentOffset.y);
}

第三个效果:如何让视图滚动的时候(不触发点击事件),自动把滚动到第几个的图片展示出来(delegate)
CGPoint pInView = [self.collectionView.superviewconvertPoint:self.collectionView.centertoView:self.collectionView];
同学如果你无法理解上面的坐标转换可以点击以下传送门,帮你快速理解

点击打开链接我要学习

// 重载第三个属性
// 滚动的时候会一直调用
// 当边界发生变化的时候,是否应该刷新布局。如果YES那么就是边界发生变化的时候,重新计算布局信息  这里的newBounds变化的只有x值的变化,也就是偏移量的变化
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    // 把collectionView的父视图(underBackView)上的中心坐标,转换到整个CollectionView ContentSize中的坐标 根据point来计算对应的indexpath
    CGPoint pInView = [self.collectionView.superview convertPoint:self.collectionView.center toView:self.collectionView];
    
    // 通过坐标获取对应的indexpath
    NSIndexPath *indexPathNow = [self.collectionView indexPathForItemAtPoint:pInView];
    
    if (indexPathNow.row == 0)
    {
        if (newBounds.origin.x < SCREEN_WIDTH / 2)
        {
            if (_index != indexPathNow.row)
            {
                _index = 0;
                if (self.delegate && [self.delegate respondsToSelector:@selector(collectioViewScrollToIndex:)])
                {
                    [self.delegate collectioViewScrollToIndex:_index];
                }
                
            }
        }
    }
    else
    {
        if (_index != indexPathNow.row)
        {
            _index = indexPathNow.row;
            if (self.delegate && [self.delegate respondsToSelector:@selector(collectioViewScrollToIndex:)])
            {
                [self.delegate collectioViewScrollToIndex:_index];
            }
        }
    }
    [super shouldInvalidateLayoutForBoundsChange:newBounds];
    return YES;
}

第四个效果:点击Item,自动中心对齐 

// 点击item的时候
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 这里的坐标相互转换 可看我写的另一个小博客
    CGPoint pInUnderView = [self.underBackView convertPoint:collectionView.center toView:collectionView];
    
    // 获取中间的indexpath
    NSIndexPath *indexpathNew = [collectionView indexPathForItemAtPoint:pInUnderView];
    
    if (indexPath.row == indexpathNew.row)
    {
        NSLog(@"点击了同一个");
        return;
    }
    else
    {
        // 点击不同就把点击的滚动到中心居中
        [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
    }
}
主要的一些方法和实现思路都已经介绍完了,小白写的Demo,大神勿喷,理解的同学可以自己试试



Demo地址:点击打开链接

Demo写的很仓促,有些细节还没做的很好,只是玩英雄联盟选择皮肤的时候突然想到要写一个试试,各位有什么问题可以留言,我会慢慢更正能阅读完博主的文章也是不容易,谢谢捧场,好人一生平安,博主会不断进步的,谢谢