二级的uicollectionview怎么实现

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二级的uicollectionview怎么实现相关的知识,希望对你有一定的参考价值。

参考技术A UICollectionView 和 UICollectionViewController 类是ios6 新引进的API,用于展示集合视图,布局更加灵活,可实现多列布局,用法类似于UITableView 和 UITableViewController 类。
使用UICollectionView 必须实现UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout这三个协议。

下面先给出常用到的一些方法。(只给出常用的,其他的可以查看相关API)
#pragma mark -- UICollectionViewDataSource
//定义展示的UICollectionViewCell的个数
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

return 30;

//定义展示的Section的个数
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

return 1;

//每个UICollectionView展示的内容
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

static NSString * CellIdentifier = @"GradientCell";
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

cell.backgroundColor = [UIColor colorWithRed:((10 * indexPath.row) / 255.0) green:((20 * indexPath.row)/255.0) blue:((30 * indexPath.row)/255.0) alpha:1.0f];
return cell;

#pragma mark --UICollectionViewDelegateFlowLayout
//定义每个UICollectionView 的大小
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

return CGSizeMake(96, 100);

//定义每个UICollectionView 的 margin
-(UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section

return UIEdgeInsetsMake(5, 5, 5, 5);

#pragma mark --UICollectionViewDelegate
//UICollectionView被选中时调用的方法
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath

UICollectionViewCell * cell = (UICollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];

//返回这个UICollectionView是否可以被选择
-(BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath

return YES;


下面通过一个例子具体介绍下。(例子来自网络。但是是通过第三方获得的,无法取得链接。还望见谅。)

iOS CollectionView的出现是一大福利,再也不用用TableView来定义复杂的多栏表格了,用法与Table类似,只是Cell必须自己添加,无默认模式
由于CollectionView没有默认的Cell布局,所以一般还是自定义方便又快捷
一、自定义Cell
1、新建类CollectionCell继承自UICollectionViewCell

2、新建Xib,命名为CollectionCell.xib

a.选中CollectionCell.xib删掉默认的View,从控件中拖一个Collection View Cell(图3)到画布中,设置大小为95*116;

b.选中刚刚添加的Cell,更改类名为CollectionCell,如图4

c.在CollectionCell.xib的CollectionCell中添加一个ImageView和一个Label(图5)

d.创建映射, 图6,图7

e.选中CollectionCell.m , 重写init方法
- (id)initWithFrame:(CGRect)frame

self = [super initWithFrame:frame];
if (self)

// 初始化时加载collectionCell.xib文件
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:@"CollectionCell" owner:self options:nil];

// 如果路径不存在,return nil
if (arrayOfViews.count < 1)

return nil;

// 如果xib中view不属于UICollectionViewCell类,return nil
if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]])

return nil;

// 加载nib
self = [arrayOfViews objectAtIndex:0];

return self;


f.选中CollectionCell.xib 修改其identifier为CollectionCell。

二、定义UICollectionView;
1、拖动一个Collection View到指定ViewController的View上
2、连线dataSource和delegate,并创建映射,命名为CollectionView
3、选中CollectionView的标尺,将Cell Size的Width和Height改成与自定义的Cell一样的95*116,图8

4、选中CollectionView的属性,可以修改其属性,比如是垂直滑动,还是水平滑动,选择Vertical或Horizontal
5、选中CollectionViewCell,修改Class,继承自CollectionCell

5、在ViewDidLoad方法中声明Cell的类,在ViewDidLoad方法中添加,此句不声明,将无法加载,程序崩溃
其中,CollectionCell是这个Cell的标识(之前几步已经定义过了。 )
[self.collectionView registerClass:[CollectionCell class] forCellWithReuseIdentifier:@"CollectionCell"];

6、在ViewController.h中声明代理
@interface ViewController : UIViewController<UICollectionViewDataSource,UICollectionViewDelegate>

7、在.m文件中实现代理方法
//每个section的item个数
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

return 12;


-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath


CollectionCell *cell = (CollectionCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"CollectionCell" forIndexPath:indexPath];

//图片名称
NSString *imageToLoad = [NSString stringWithFormat:@"%d.png", indexPath.row];
//加载图片
cell.imageView.image = [UIImage imageNamed:imageToLoad];
//设置label文字
cell.label.text = [NSString stringWithFormat:@"%ld,%ld",(long)indexPath.row,(long)indexPath.section];

return cell;


8 。效果如图10

点击某项后跳转事件与UITableView类似,实现代理方法
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
即可,不赘述本回答被提问者和网友采纳

05京东淘宝首页二级联动怎么实现

(1)自定义View中的事件分发流程
(2)嵌套滑动冲突
(3)嵌套滑动冲突解决方案
(4)嵌套滑动及吸顶效果制作
(5)嵌套滑动吸顶效果滑动冲突解决方案
(6)嵌套滑动吸顶效果中的惯性滑动处理
(7)事件的内部拦截与外部拦截

【05】京东淘宝首页二级联动怎么实现

文章目录

1.概述

1.1Android本质

(1)四大组件
(2)自定义View

1.2自定义View需要了解什么?

(1)自定义View的创建及渲染流程

  • onMeasure 测量
  • onLayout 摆放
  • onDraw 绘制

(2)事件分发

  • dispatch
  • intercept
  • onTouch

(3)滑动冲突

滑动冲突有哪两种解决方案?

  • 内部拦截与外部拦截

(4)嵌套滑动(是滑动冲突的进阶)

嵌套滑动有几个版本?

  • 3个版本。

  • 大厂APP很多在用

2.案例布局分析

2.1为什么滑动事件会被吃掉?

(1)与事件分发有关系

2.1.1view与ViewGroup的关系

(1)从代码层面来讲,ViewGroup继承了View。
(2)从运行角度来讲,ViewGroup是View的父亲。
(3)事件分发是根据运行角度来的。
(4)事件分发是如何来分发的?

  • 是从Activity开始的。

2.1.2.单点触摸

2.1.3多点触摸

2.1.3.1多点手势手指操作流程

(1)手势

(2)流程

(3)一个Move事件有几个手指的信息?

  • 有几个手指就有几个.
  • 最多不能超过32个手指信息.

3.事件分发的流程

(1)从Activity开始分发

  • android.app.Activity#dispatchTouchEvent
  • android.view.Window.Callback#dispatchTouchEvent
  • android.view.Window#superDispatchTouchEvent
  • com.android.internal.policy.PhoneWindow#superDispatchTouchEvent
  • com.android.internal.policy.DecorView#superDispatchTouchEvent

(2)经过以上步骤,事件被分发到DecorView,DecorView继续分发事件
android.view.ViewGroup#dispatchTouchEvent

  • DecorView继承自FrameLayout
  • 而FrameLayout继承自ViewGroup

(3)事件是什么时候开始的

  • 从ACTION_DOWN开始(不管是单点触摸还是多点触摸,一个手势的MOVE都是从ACTION_DOWN开始的)

(4)DecorView分发到具体的布局时,会通过一个函数判断是否继续分发事件。
onInterceptTouchEvent

(5)android.view.ViewGroup#dispatchTouchEvent

if (onFilterTouchEventForSecurity(ev)) 
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
    		//判断是否是一个新事件的开始
            if (actionMasked == MotionEvent.ACTION_DOWN) 
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                //如果是一个新的事件,需要清除掉所有的事件相关的东西
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            

            // Check for interception.
    		//用一个局部变量标记是否拦截事件
    		//如果是一个手势,同时还没有分发给其他人
    		//mFirstTouchTarget是一个存储事件的链表,表示有哪几个View来接收事件,有可能是一个手指触摸到
    		//多个View
    		//如果是多个手指放到多个View的时候,在不同的View层次的时候,才会进入到多个View里面去。
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) 
               /*
                *1.滑动冲突
                *(1)有内部拦截与外部拦截的
                *(2)mGroupFlags用于标识内部拦截,查看disallowIntercept这个标识是否允许拦截
                *(3)子View有权利申请父亲不要拦截事件,即通过disallowIntercept标识申请
                *(4)而disallowIntercept变量的值只能通过android.view.ViewGroup#requestDisallowInterceptTouchEvent方法进行修改。
                *(5)事件分发的时候要去看一下孩子是否告诉我不能够拦截孩子的事件。
                *(6)如果说事件要拦截,就交给onTouchEvent()进行处理
                *(7)如果说不拦截,就会一直分发下去,分发到什么状态呢?
                */
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                
                if (!disallowIntercept) 
                    //如果不允许拦截,就会问onInterceptTouchEvent                   
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                 else 
                    intercepted = false;
                
             else 
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) 
                ev.setTargetAccessibilityFocus(false);
            

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不是cancel与拦截
            if (!canceled && !intercepted) 
                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) 
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) 
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //一个一个去问,
                        for (int i = childrenCount - 1; i >= 0; i--) 
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) 
                                continue;
                            

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) 
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            

                            resetCancelNextUpFlag(child);
                            //如果遇到一个孩子要处理事件,继续分发,返回为true了,
                            //分发事件是分发给一个View,链表记录的也是一个View
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) 
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) 
                                        if (children[childIndex] == mChildren[j]) 
                                            mLastTouchDownIndex = j;
                                            break;
                                        
                                    
                                 else 
                                    mLastTouchDownIndex = childIndex;
                                
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //将该孩子添加到touchTarget中去,会改变mFirstTouchTarget,链表发生改变
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        
                        if (preorderedList != null) preorderedList.clear();
                    

                    if (newTouchTarget == null && mFirstTouchTarget != null) 
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) 
                            newTouchTarget = newTouchTarget.next;
                        
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    
                
            

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) 
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
             else 
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) 
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) 
                        handled = true;
                     else 
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) 
                            handled = true;
                        
                        if (cancelChild) 
                            if (predecessor == null) 
                                mFirstTouchTarget = next;
                             else 
                                predecessor.next = next;
                            
                            target.recycle();
                            target = next;
                            continue;
                        
                    
                    predecessor = target;
                    target = next;
                
            

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) 
                resetTouchState();
             else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) 
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            
        

3.1如何解决嵌套滑动冲突?

3.1.1嵌套滑动产生原因

(1)案例中,事件已经给了里层的recycleView,已经将事件给消费掉了,导致无法将事件分发到外层的控件ScrollView,也就导致外部的ScrollView无法滚动。即在不支持嵌套滑动时,无法将事件向外层事件分发。

  • 外层使用的是ScrollView
  • 嵌套滑动,里层的RecycleView属于外层ScrollView的孩子。
  • RecycleView实现了嵌套滑动的孩子,NestedScrollingChild2, NestedScrollingChild3.
  • 嵌套滑动,一定要有两个角色参与,嵌套滑动要有父亲的角色,要有孩子的角色.案例1中一个是父亲(ScrollView),一个是孩子(RecyclerView)。
  • 而RecyclerView实现了孩子的角色,NestedScrollingChild2, NestedScrollingChild3,而ScrollView没有实现父亲的角色。ScrollView作为父控件,没有实现NestedScrollingParent3,即不具备父亲的角色,导致无法嵌套滑动。
public class ScrollView extends FrameLayout 
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 

3.1.2嵌套滑动解决方法

  • 换成NestedScrollView来解决问题,它实现了NestedScrollingParent3,NestedScrollingChild3
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
        NestedScrollingChild3, ScrollingView 

3.2吸顶效果

(1)固定位置
(2)事件拦截
(3)备胎

(4)将tablayout+viewpager的高度设置为屏幕的高度

  • 自定义NestedScrollView实现吸顶效果
    com.gdc.knowledge.highui.jdtb.nestedscroll.c_fixedheight_viewpager_nestedscrollview_recyclerview.NestedScrollLayoutTest
/**
     * 1.当布局加载完成时,获取需要做吸顶效果的View(TabLayout+ViewPager区域部分)
     * 2.在测量的过程中,修改该布局区域的高度为整个屏幕的高度,即可出现吸顶效果
     */
    @Override
    protected void onFinishInflate() 
        super.onFinishInflate();
        contentView = (ViewGroup) ((ViewGroup) getChildAt(0)).getChildAt(1);
    

    /**
     * 1.调整contentView的高度为父容器的高度,使之填充(布局)整个屏幕的高度
     * 即产生吸顶效果,避免父容器滚动后出现空白。
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams lp = contentView.getLayoutParams();
        lp.height = getMeasuredHeight();
        contentView.setLayoutParams(lp);
    
  • 布局
    activity_nested_view_pager_test3.xml

3.3做了吸顶效果后无法上滑

(1)希望达到的效果

如果在滑动RecyclerView的时候,可以先判断一下NestedScrollView是否还可以继续滑动,如果可以继续滑动,则先让其向上滑动完成,当他不能往上滑的时候,就让RecyclerView自己滑。

(2)孩子滑动有3个版本,3继承2,2继承1

  • 版本2比版本1多了个嵌套滑动类型的参数
    一种叫TYPE_TOUCH:惯性滑动,是手指已经离开了屏幕,但是还有剩余的力量支撑滑动。即手指还在屏幕上,用的力气很大,手指离开以后还要继续滑。

一种是TYPE_NON_TOUCH:手指滑动

public interface NestedScrollingChild3 extends NestedScrollingChild2 
public interface NestedScrollingChild2 extends NestedScrollingChild 

(3)父亲与孩子的关系

3.4嵌套滑动流程

3.3.1ACTION_DOWN

  • 嵌套滑动虽然有父亲与孩子两个角色,但是主动者是孩子,事情是由孩子触发的。

  • 如上图,如果在RecyclerView滑动之前,希望NestedScrollView先滑完.

  • 案例2没有实现的原因

  • 一进入到界面时,需要设置RecyclerVeiw支持嵌套滑动

  • 监测RecyclerView嵌套滑动事件流程
    com.gdc.knowledge.highui.jdtb.common.fragment.NestedLogRecyclerView

  • 滑动事件帮助类
    androidx.core.view.NestedScrollingChildHelper
    androidx.core.view.NestedScrollingChildHelper#startNestedScroll(int, int)

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) 
    //判断是否有嵌套滑动的父亲,首次没有
    if (hasNestedScrollingParent(type)) 
            // Already in progress
            return true;
        
   /**
   	1.如果没有嵌套滑动的父亲,就判断是否支持嵌套滑动
   	2.就一直去找嵌套滑动的父亲
    */
        if (isNestedScrollingEnabled()) 
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) 
                //3.如果找到了,就判断其是否支持嵌套滑动,执行相应的嵌套滑动方法
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) 
                    setNestedScrollingParentForType(type, p);
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                
                //4.如果不支持嵌套滑动,就会一直往上去找,找它的父亲
                if (p instanceof View) 
                    child = (View) p;
                
                p = p.getParent();
            
        
        return false;
    

(4)找到支持嵌套滑动的父亲之后,执行相应的嵌套滑动方法

@Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
            int type) 
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    

NestedScrollView只支持垂直方向的滑动,横向的不支持。

3.3.2ACTION_MOVE

在滑动之前会执行dispatchNestedPreScroll方法
androidx.core.view.NestedScrollingChildHelper#dispatchNestedPreScroll(int, int, int[], int[], int),NestedScrollView是先交给了它的父亲滑动,它即是孩子又是父亲,有控件询问自己是否可以滑.

/**
 * @author XiongJie
 * @version appVer
 * @Package com.gdc.knowledge.highui.jdtb.nestedscroll.e_prefect_nestedscroll
 * @file
 * @Description:
 * 1.解决嵌套滑动吸顶效果滑动冲突问题
 * (1)当滑动内层RecycleView时,判断外层NestedScrollLayout自定义View是否还可以继续滑动
 * (2)如果可以继续滑动,则先让其滑动完
 * (3)如果父级NestedScrollLayout不能滑动了,则让
 * @date 2021-6-30 16:10
 * @since appVer
 */

public class NestedScrollLayout extends NestedScrollView 

    //布局
    private View topView;
    private ViewGroup contentView;
    private static final String TAG = "NestedScrollLayout";
    /**
     * 惯性滑动时使用到的工具类
     */
    private FlingHelper mFlingHelper;
    /**
     *在RecyclerView fling(惯性滑动)情况下,记录当前RecyclerView在y轴的偏移
     */
    int totalDy = 0;
    /**
     * 用于判断RecyclerView是否在fling惯性滑动
     */
    boolean isStartFling = false;
    /**
     * 记录当前滑动的y轴加速度
     */
    private int velocityY = 0;

    public NestedScrollLayout(@NonNull Context context) 
        super(context);
        init();
    

    public NestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) 
        super(context, attrs);
        init();
    

    public NestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr)使用 UICollectionView 实现首页卡片轮播效果

如何实现优步 V2 UICollectionView

UICollectionView(I)

折叠UICollectionView

ios uicollectionview怎么实现滚动

怎么实现UICollectionView放大,并且左右可以拖动