精华内容
下载资源
问答
  • 把握生命里的每一分钟,全力以赴我们心中的梦,不经历风雨,怎么见彩虹,没有人能随随便便成功 -----《真心英雄》 在上一章中,我们讲了RecyclerView的各种基础知识,在这章中,我们将通过非常炫酷的特效来实际学习...

    把握生命里的每一分钟,全力以赴我们心中的梦,不经历风雨,怎么见彩虹,没有人能随随便便成功 -----《真心英雄》


    系列文章: Android自定义控件三部曲文章索引: http://blog.csdn.net/harvic880925/article/details/50995268


    本节是我的新书《Android自定义控件高级进阶与精彩实例》中的一小节,目前还在著作中,预计2020年上市,本来没打算更出来,可有些同学评论非常需要这个效果,就摘取出来分享给大家,前面的章节序号我就不改了……(我太懒)

    在上一章中,我们讲了RecyclerView的各种基础知识,在这章中,我们将通过非常炫酷的特效来实际学习下RecyclerView。可以看到,通过这些看似平淡的功能,能做出非常漂亮的控件,现在我们就开始吧。

    5.1 滚动画廊控件

    本节将实现在上一章中提到过的画廊效果,但为了减轻难度,就不再制作3D画廊,而是制作出2D的,不过最后将在2D的基础上,讲解3D画廊的实现原理,本节实现的效果如下图所示:
    在这里插入图片描述

    高能预警:本节代码量较大,而且是利用4.5节代码修改而来,对于同一个函数因为逻辑实现次序的问题,可能会多次修改,而且由于篇幅有限,并不能每次贴出全部源码,只能截取核心部分,所以建议大家对照着源码看本节文章,不然半路有可能会蒙……。

    5.1.1 实现Item布局

    这节内容,我们实现原理与4.5节基本相同,所以很多代码,大家理解起来应该都不难,有些部分就不再细讲。在这部分,我们先在4.5节代码的基础上做修改,以便很快可以看到效果。首先,我们先把4.5节中的item布局更改为我们想要的布局(item_coverflow.xml)

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    
        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAlignment="center"
            android:text="0"
            android:layout_gravity="center"
            android:textColor="@android:color/black"/>
    
        <ImageView
            android:id="@+id/img"
            android:layout_marginTop="10dp"
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:scaleType="centerCrop"/>
    </LinearLayout>
    

    布局很好理解,就是垂直排列一个text和一个img。text用于显示当前item的位置,img用于显示图片。

    所以,我们还需要引用几个图片资源,源码中放在mipmap-xxhdpi文件夹下:

    在这里插入图片描述
    然后,我们新建一个Adapter(CoverFlowAdapter):

    public class CoverFlowAdapter extends Adapter<ViewHolder> {
    
        private Context mContext;
        private ArrayList<String> mDatas;
        private int mCreatedHolder=0;
        private int[] mPics = {R.mipmap.item1,R.mipmap.item2,R.mipmap.item3,R.mipmap.item4,
                R.mipmap.item5,R.mipmap.item6};
        public CoverFlowAdapter(Context context, ArrayList<String> datas) {
            mContext = context;
            mDatas = datas;
        }
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            mCreatedHolder++;
            LayoutInflater inflater = LayoutInflater.from(mContext);
            return new NormalHolder(inflater.inflate(R.layout.item_coverflow, parent, false));
        }
    
        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            NormalHolder normalHolder = (NormalHolder) holder;
            normalHolder.mTV.setText(mDatas.get(position));
            normalHolder.mImg.setImageDrawable(mContext.getResources().getDrawable(mPics[position%mPics.length]));
        }
    
        @Override
        public int getItemCount() {
            return mDatas.size();
        }
    
        public class NormalHolder extends ViewHolder {
            public TextView mTV;
            public ImageView mImg;
    
            public NormalHolder(View itemView) {
                super(itemView);
    
                mTV = (TextView) itemView.findViewById(R.id.text);
                mTV.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(mContext, mTV.getText(), Toast.LENGTH_SHORT).show();
                    }
                });
    
                mImg = (ImageView)itemView.findViewById(R.id.img);
                mImg.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(mContext, mTV.getText(), Toast.LENGTH_SHORT).show();
                    }
                });
    
            }
        }
    }
    

    代码理解起来应该难度不大,首先,我们新建一个NormalHolder,来保存布局中的控件所对应的变量。然后在onCreateViewHolder中返回新建的NormalHolder对象,最后通过onBindViewHolder将NormalHolder与数据绑定起来。

    此时运行代码,可以看到效果如下图所示:

    在这里插入图片描述
    因为我们在4.5节中,给每个item在滚动时,都设置了setRotationY,所以我们在滚动时,每个item都还会旋转。我们这节中并不需要让item旋转,所以我们在自定义的LayoutManager中删除child.setRotationY(child.getRotationY() + 1);代码。

    修改后的效果如下图所示:
    在这里插入图片描述

    5.1.2 实现横向布局

    5.1.2.1 开启横向滚动

    现在还是4.5节中所实现的竖向滚动,现在我们要把它改为横向滚动。首先,我们需要删除canScrollVertically()scrollVerticallyBy函数,改为:

    @Override
    public boolean canScrollHorizontally() {
        return true;
    }
    
    @Override
    public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    	…………
    }
    

    在将scrollVerticallyBy改为scrollHorizontallyBy以后,需要把原来在scrollVerticallyBy中的代码移到scrollHorizontallyBy中来。

    很明显,现在运行的话,虽然可以成功运行,但依然是竖向布局。当然是因为我们在onLayoutChildren中,在布局时,并没有将每个item横向布局的原因。

    5.1.2.2 实现横向布局

    最关键的问题,就是我们在初始化时,会利用mItemRects来保存所有item的位置,所以在计算每个item位置时,改为横向布局的方式来计算即可:

    int offsetX = 0;
    
    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
        mItemRects.put(i, rect);
        mHasAttachedItems.put(i, false);
        offsetX += mItemWidth;
    }
    

    然后在获取visibleCount时,需要修改为:

    int visibleCount = getHorizontalSpace() / mItemWidth;
    

    同时,在onLayoutChildren最后,有个计算mTotalHeight的逻辑,我们需要改为计算totalWidth的逻辑:

    @Override
    public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
    	…………
        mTotalWidth = Math.max(offsetX, getHorizontalSpace());
    }
    
    private int getHorizontalSpace() {
        return getWidth() - getPaddingLeft() - getPaddingRight();
    }
    

    在这段代码中,我们让所有item都靠顶部横向依次排列,难度不大,不再细讲。

    同时,在getVisibleArea函数也需要修改,因为我们现在已经是横向滚动了,已经不再是竖向滚动了,所以可见区域应该是横向滚动后的可见区域:

    private Rect getVisibleArea() {
        Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
        return result;
    }
    

    onLayoutChildren函数中的其它代码不需要更改,此时onLayoutChildren的代码如下:

    public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {//没有Item,界面空着吧
            detachAndScrapAttachedViews(recycler);
            return;
        }
        mHasAttachedItems.clear();
        mItemRects.clear();
    
        detachAndScrapAttachedViews(recycler);
    
        //将item的位置存储起来
        View childView = recycler.getViewForPosition(0);
        measureChildWithMargins(childView, 0, 0);
        mItemWidth = getDecoratedMeasuredWidth(childView);
        mItemHeight = getDecoratedMeasuredHeight(childView);
    
        int visibleCount = getVerticalSpace() / mItemWidth;
    
    
        //定义水平方向的偏移量
        int offsetX = 0;
    
        for (int i = 0; i < getItemCount(); i++) {
            Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
            mItemRects.put(i, rect);
            mHasAttachedItems.put(i, false);
            offsetX += mItemWidth;
        }
    
        Rect visibleRect = getVisibleArea();
        for (int i = 0; i < visibleCount; i++) {
            insertView(i, visibleRect, recycler, false);
        }
    
        //如果所有子View的宽度和没有填满RecyclerView的宽度,
        // 则将宽度设置为RecyclerView的宽度
        mTotalWidth = Math.max(offsetX, getHorizontalSpace());
    }
    
    private int getHorizontalSpace() {
        return getWidth() - getPaddingLeft() - getPaddingRight();
    }
    
    private Rect getVisibleArea() {
        Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
        return result;
    }
    

    同时,我们需要该View所在的Activity改为横向展示:

    <activity android:name=".CoverFlowActivity"
        android:screenOrientation="landscape"/>
    

    修改后的效果如下图所示:
    在这里插入图片描述
    很明显,现在在初始化时,已经可以实现横向布局了,但如果你一滚动就出现异常了,这是很正常的,毕竟我们还没有处理滚动事件。

    5.1.3 实现横向滚动

    横向滚动是放在scrollHorizontallyBy中处理,其中滑动顶部判断,越界处理这些都是相同的,只是在布局item时,需要重新写。

    滚动时布局item涉及到两个地方:

    第一:在回收越界时,对于已经在屏幕上显示的item,重新layout时:

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        View child = getChildAt(i);
        int position = getPosition(child);
        Rect rect = mItemRects.get(position);
    
        if (!Rect.intersects(rect, visibleRect)) {
            removeAndRecycleView(child, recycler);
            mHasAttachedItems.put(position, false);
        } else {
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
            mHasAttachedItems.put(position, true);
        }
    }
    

    这里只需要修改layoutDecoratedWithMargins函数即可,在布局时,根据mSumDx布局item的left和right坐标:layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);,因为是横向布局,所以top和bottom都不变。

    第二:在新移动出来的空白区域填充item时,同样涉及layout操作,同样需要处理:

    private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
        Rect rect = mItemRects.get(pos);
        if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
            View child = recycler.getViewForPosition(pos);
            if (firstPos) {
                addView(child, 0);
            } else {
                addView(child);
            }
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
    
            mHasAttachedItems.put(pos, true);
        }
    }
    

    到这里,完整的横向滚动效果就实现出来了,效果如下图所示:
    在这里插入图片描述
    scrollHorizontallyBy完整的代码如下:

    public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
        if (getChildCount() <= 0) {
            return dx;
        }
    
        int travel = dx;
        //如果滑动到最顶部
        if (mSumDx + dx < 0) {
            travel = -mSumDx;
        } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
            //如果滑动到最底部
            travel = mTotalWidth - getHorizontalSpace() - mSumDx;
        }
    
        mSumDx += travel;
    
        Rect visibleRect = getVisibleArea();
    
        //回收越界子View
        for (int i = getChildCount() - 1; i >= 0; i--) {
            View child = getChildAt(i);
            int position = getPosition(child);
            Rect rect = mItemRects.get(position);
    
            if (!Rect.intersects(rect, visibleRect)) {
                removeAndRecycleView(child, recycler);
                mHasAttachedItems.put(position, false);
            } else {
                layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
                mHasAttachedItems.put(position, true);
            }
        }
    
    	//填充空白区域
        View lastView = getChildAt(getChildCount() - 1);
        View firstView = getChildAt(0);
        if (travel >= 0) {
            int minPos = getPosition(firstView);
            for (int i = minPos; i < getItemCount(); i++) {
                insertView(i, visibleRect, recycler, false);
            }
        } else {
            int maxPos = getPosition(lastView);
            for (int i = maxPos; i >= 0; i--) {
                insertView(i, visibleRect, recycler, true);
            }
        }
        return travel;
    }
    private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
        Rect rect = mItemRects.get(pos);
        if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
            View child = recycler.getViewForPosition(pos);
            if (firstPos) {
                addView(child, 0);
            } else {
                addView(child);
            }
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
    
            mHasAttachedItems.put(pos, true);
        }
    }
    

    由于篇幅有限,后面onLayoutChildren和scrollHorizontallyBy的代码仅讲解修改部分,不再重新列出所有源码,仅列出这一次。

    5.1.4 实现卡片叠加

    从最终的效果图中可以看出,我们两个卡片之间并不是并排排列的,而是叠加在一起的。在这个例子中,两个卡片之间叠加的部分是半个卡片的大小。所以,我们需要修改排列卡片的代码,使卡片叠加起来。

    首先,申请一个变量,保存两个卡片之间的距离:

    private int mIntervalWidth;
    
    private int getIntervalWidth() {
        return mItemWidth / 2;
    }
    

    其中getIntervalWidth()函数用于向mIntervalWidth变量赋值。

    然后在onLayoutChildren中,首先给mIntervalWidth初始化,然后在计算每个卡片的起始位置时,offsetX每次位移距离,改为offsetX += mIntervalWidth,具体代码如下:

    mIntervalWidth = getIntervalWidth();
    
    //定义水平方向的偏移量
    int offsetX = 0;
    
    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
        mItemRects.put(i, rect);
        mHasAttachedItems.put(i, false);
        offsetX += mIntervalWidth;
    }
    

    这里需要注意的是,在计算每个卡片的位置时Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight),在这个Rect的right位置,不能改为offsetX + mIntervalWidth,因为我们只是更改了卡片布局时的起始位置,并没有更改卡片的大小,所以每个卡片的长度和宽度是不能变的。

    然后在初始化时插入item时,在计算visibleCount时,需要改为int visibleCount = getHorizontalSpace() / mIntervalWidth,代码如下:

    int visibleCount = getHorizontalSpace() / mIntervalWidth;
    Rect visibleRect = getVisibleArea();
    for (int i = 0; i < visibleCount; i++) {
        insertView(i, visibleRect, recycler, false);
    }
    

    因为在scrollHorizontallyBy中处理滚动时,每个卡片的位置都是直接从mItemRects中取的,所以,我们并不需要在修改滚动时的代码。

    到这里,就实现了卡片叠加的功能,效果如下图所示:
    在这里插入图片描述

    5.1.5 修改卡片起始位置

    到现在,我们卡片都还是在最左侧开始展示的,但在开篇的效果图中可以看出,在初始化时,第一个item是在最屏幕中间显示的,这是怎么做到的呢?

    首先,我们需要先申请一个变量mStartX,来保存卡片后移的距离。

    很明显,这里也只是改变每个卡片的布局位置,所以我们也只需要在onLayoutChildren中,在mItemRects中初始化每个item位置时,将每个item后移mStartX就可以了。

    所以核心代码如下:

    private int mStartX;
    
    public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
    
    	…………
    	mStartX = getWidth()/2 - mIntervalWidth;
    
        //定义水平方向的偏移量
        int offsetX = 0;
        for (int i = 0; i < getItemCount(); i++) {
            Rect rect = new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight);
            mItemRects.put(i, rect);
            mHasAttachedItems.put(i, false);
            offsetX += mIntervalWidth;
        }
    	…………
    }
    

    首先,是mStartX的初始化,因为我们需要第一个卡片的中间位置在屏幕正中间的位置,从下图中明显可以看出,mStartX的值应该是:mStartX = getWidth()/2 - mIntervalWidth;

    在这里插入图片描述
    然后,在计算每个item的rect时,将每个item后移mStartX距离:new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight)

    就这样,我们就完成了移动初始化位置的功能,效果如下图所示:
    在这里插入图片描述

    5.1.6 更改默认显示顺序

    5.1.6.1 更改默认显示顺序的原理

    现在,我们每个item的显示顺序还是后一个卡片压在前一个卡片上显示的,这是因为,在RecyclerView绘制时,先绘制的第一个item,然后再绘制第二个item,然后再绘制第三个item,……,默认就是这样的绘制顺序。即越往前的item越优先绘制。绘制原理示图如下:

    在这里插入图片描述
    这里显示的三个item绘制次序,很明显,正是由于后面的item把前面的item叠加部分盖住了,才造成了现在的每个item只显示出一半的情况。

    那如果我们更改下显示顺序,将两边的先绘制,将屏幕中间的Item(当前选中的item)最后绘制,就会成为这个情况:

    在这里插入图片描述
    形成的效果就是本节开篇的效果(这个效果中还有缩放,在下小节中讲解):

    在这里插入图片描述
    那关键的部分来:要怎么更改Item的绘制顺序呢?

    其实,只需要重写RecyclerView的getChildDrawingOrder方法即可。

    该方法的详细声明如下:

    protected int getChildDrawingOrder(int childCount, int i)
    
    • childCount:表示当前屏幕上可见的item的个数
    • i:表示item的索引,一般而言,i的值就是在list中可见item的顺序,通过getChildAt(i)即可得到当前item的视图。
    • return int:返回值表示当前item的绘制顺序,返回值越小,越先绘制,返回值越大,越最后绘制。很显然,要实现我们开篇的效果,中间item的返回值应该是最大的,才能让它最后绘制,以显示在最上面。

    需要注意的是,默认情况下,即便重写getChildDrawingOrder函数,代码也不会执行到getChildDrawingOrder里面的,我们需要在RecyclerView初始化时,显式调用setChildrenDrawingOrderEnabled(true);开启重新排序。

    所以开启重新排序,总共需要有两步:

    1.调用setChildrenDrawingOrderEnabled(true);开启重新排序
    2.在getChildDrawingOrder中重新返回每个item的绘制顺序

    5.1.6.2 重写RecyclerView

    因为我们要重写getChildDrawingOrder,所以我们必须重写RecylcerView:

    public class RecyclerCoverFlowView extends RecyclerView {
        public RecyclerCoverFlowView(Context context) {
            super(context);
            init();
        }
    
        public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init(){
            setChildrenDrawingOrderEnabled(true); //开启重新排序
        }
    
        /**
         * 获取LayoutManger,并强制转换为CoverFlowLayoutManger
         */
        public CoverFlowLayoutManager getCoverFlowLayout() {
            return ((CoverFlowLayoutManager)getLayoutManager());
        }
    
        @Override
        protected int getChildDrawingOrder(int childCount, int i) {
           return super.getChildDrawingOrder(childCount, i);
        }
    }
    

    在这里,我们主要做了两步:

    1. 在初始化时,使用setChildrenDrawingOrderEnabled(true);开启重新排序
    2. 因为后面,我们需要用到我们自定义的LayoutManager,所以我们额外提供了一个函数public CoverFlowLayoutManager getCoverFlowLayout(),以供后续使用

    接下来,我们就来看看如何在getChildDrawingOrder中返回对应item的绘制顺序。

    5.1.6.3 计算绘制顺序原理

    下图展示了位置索引与绘图顺序的关系:

    在这里插入图片描述
    在这个图中,总共有7个Item,带有圆圈的0,1,2,3,4,5,6是当前在屏幕中显示的item位置索引,它的值也是默认的绘图顺序,默认的绘图顺序就是越靠前的item越先绘制。

    要想达到图上所示的效果,它的绘图顺序可以是0,1,2,6,5,4,3;因为数值代表的是绘制顺序,所以值越大的越后绘制,所以左侧的三个的顺序是0,1,2;所以,第一个item先绘制,然后第二个item盖在第一个上面;再然后,第三个item再绘制,它会盖在第二个item的上面。所以这样就保证的中间卡片左侧部分的叠加效果。右侧三个的绘制顺序是5,4,3;所以最后一个item先绘制,然后是倒数第二个,最后是倒数第三个;同样,右侧三个也可以保证图中的叠加效果。最中间的Item绘制顺序为6,所以最后绘制,所以它会盖在所有item的上面显示出来。
    注意:我这里讲到这个效果的绘图顺序时,说的是“可以是”,而不是“必须是”!,只要保证下面两点,所有的绘图顺序都是正确的:

    1. 绘图顺序的返回值范围在0到childCount-1之间,其中(childCount表示当前屏幕中的可见item个数)
    2. 此绘图顺序在叠加后,可以保证最终效果

    所以,如果我们把绘图顺序改为3,4,5,6,2,1,0;同样是可以达到上面的效果的。

    为了方便计算规则,我们使用0,1,2,6,5,4,3的绘图顺序。

    很明显,我们需要先找到所有在显示item的中间位置,中间位置的绘图顺序是count -1;

    然后中间位置之前的绘图顺序和它的排列排序相同,在getChildDrawingOrder函数中,排列顺序是i,那么绘图顺序也是i;

    最难的部分是中间位置之后的部分,它们的绘图顺序怎么算。

    很明显,最后一个item的绘图顺序始终是center(指屏幕显示的中间item的索引,这里是3)。倒数第二个的绘图顺序是center+1,倒数第三个的绘图顺序是center+2;从这个计算中可以看出,后面的item的绘图顺序总是center+m,而m的值就是当前的item和最后一个item所间隔的个数。那当前item和最后一个item间隔的个数怎么算呢?它等于count - 1 - i;不知道大家能不能理解,count-1正常显示顺序下最后一个item的索引,也就是当前可见的item中的最大的索引,而i是屏幕中显示的item的索引,也就是上图圆圈内的数值。所以,中间后面的item的绘图顺序的计算方法是center + count - 1- i;

    需要非常注意的是,这里的i是指屏幕中显示item的索引,总是从0开始的,并不是指在Adapter中所有item中的索引值。它的意义与getChildAt(i)中的i是一样的。

    所以总结来讲:

    • 中间位置的绘图顺序为order = count -1;
    • 中间位置之前的item的绘图顺序为 order = i;
    • 中间位置之后的item的绘图顺序为 order = center + count - i - i;

    5.1.6.4 重写getChildDrawingOrder

    在理解了如何计算绘图顺序以后,现在就开始写代码了,在上面总结中,可以看到,这里count和 i 都是getChildDrawingOrder中现成的,唯一缺少的就是center值。center值是当前可见item中间位置从0开始的索引。我们可以通过中间位置的position减去第一个可见的item的position得到。

    所以,我们需要在CoverFlowLayoutManager中添加一个函数(获取中间item的positon–指在adapter中的position):

    public int getCenterPosition(){
        int pos = (int) (mSumDx / getIntervalWidth());
        int more = (int) (mSumDx % getIntervalWidth());
        if (more > getIntervalWidth() * 0.5f) pos++;
        return pos;
    }
    

    因为我们每个item的间隔都是getIntervalWidth(),所以通过mSumDx / getIntervalWidth()就可以知道当前移到了多少个item了。因为我们已经将第一个item移到了中间,所以这里的pos就是移动mSumDx以后,中间位置item的索引。
    但是又因为我们通过mSumDx / getIntervalWidth()取整数时,它的结果是向下取整的。所以,但是我们想要在中间item移动时,超过一半就切换到下一个item显示。所以我们需要做一个兼容处理:

    int more = (int) (mSumDx % getIntervalWidth());
    if (more > getIntervalWidth() * 0.5f) pos++;
    

    利用(int) (mSumDx % getIntervalWidth())得到当前正在移动的item移动过的距离,如果more大于半个item的话,那就让pos++,将下一个item标记为center,从而让它最后绘制,显示在最上层。

    在得到中间位置的position之后,我们还需要得到第一个可见的item的position:

    public int getFirstVisiblePosition() {
        if (getChildCount() <= 0){
            return 0;
        }
    
        View view = getChildAt(0);
        int pos = getPosition(view);
        
        return pos;
    }
    

    这里的原理也非常简单,就是利用getChildAt(0)得到当前在显示的,第一个可见的item的View,然后通过getPosition(View)得到这个view在Adapter中的position。

    接下来,我们就重写getChildDrawingOrder,根据原理可得如下代码:

    protected int getChildDrawingOrder(int childCount, int i) {
        int center = getCoverFlowLayout().getCenterPosition()
                - getCoverFlowLayout().getFirstVisiblePosition(); //计算正在显示的所有Item的中间位置
        int order;
    
        if (i == center) {
            order = childCount - 1;
        } else if (i > center) {
            order = center + childCount - 1 - i;
        } else {
            order = i;
        }
        return order;
    }
    

    在获得绘图顺序的原理理解了之后,上面的代码就没有难度了,这里就不再细讲了。到这里,我们就实现了通过更改绘图顺序的方式,让当前选中的item在中间全部展示出来。

    在我们的布局中,需要使用新定义的RecyclerView,所以将原布局改为:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical"
                  tools:context=".LinearActivity">
    
        <com.example.harvic.blogrecyclerviewsec.RecyclerCoverFlowView
            android:id="@+id/linear_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </LinearLayout>
    

    这样,我们修改绘制顺序的代码就完成了,效果如下图所示:

    在这里插入图片描述

    5.1.7 添加滚动缩放功能

    5.1.7.1 代码实现

    在讲解《RecyclerView回收实现方式二》时,我们就已经实现了,在滚动时让Item旋转的功能,其实非常简单,只需要在layoutDecoratedWithMargins后,调用setRotate系列函数即可,同样的,我们先写一个针对刚添加的ChildView进行缩放的函数:

    private void handleChildView(View child,int moveX){
        float radio = computeScale(moveX);
    
        child.setScaleX(radio);
        child.setScaleY(radio);
    }
    
    private float computeScale(int x) {
        float scale = 1 -Math.abs(x * 1.0f / (8f*getIntervalWidth()));
        if (scale < 0) scale = 0;
        if (scale > 1) scale = 1;
        return scale;
    }
    

    在这两个函数中,handleChildView函数非常容易理解,就是先通过computeScale(moveX)计算出一个要缩放的值,然后调用setScale系列函数来缩放

    这里先实现效果,至于computeScale(moveX)里的公式是如何得来的,我们最后再讲解,这里先用着。

    接着,我们需要把handleChildView放在所有的layoutDecoratedWithMargins后,进行对刚布局的view进行缩放:

    public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
    
        …………
    
        //回收越界子View
        for (int i = getChildCount() - 1; i >= 0; i--) {
    		…………
            if (!Rect.intersects(rect, visibleRect)) {
                removeAndRecycleView(child, recycler);
                mHasAttachedItems.put(position, false);
            } else {
                layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
                handleChildView(child,rect.left - mStartX - mSumDx);
                mHasAttachedItems.put(position, true);
            }
        }
        …………
    }
    
    private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
        …………
        if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
            …………
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
    		handleChildView(child,rect.left - mStartX - mSumDx);
            mHasAttachedItems.put(pos, true);
            
        }
    }
    

    到这里,我们就实现了开篇的效果:

    在这里插入图片描述

    5.1.7.2 缩放系数计算原理

    我们要实现在卡片滑动时平滑的缩放,所以,在滑动过程中得到的缩放因子肯定是要连续的,所以它的函数必定是可以用直线或者曲线表示的。

    在这里,我直接用一条直线来计算滚动过程中缩放因子,此直线如下图所示:

    在这里插入图片描述

    • Y轴:表示图片的缩放比例
    • X轴:表示item距离中心点的距离。很明显,当中间的item的左上角在mStartX上时,此时距离中心点的距离为0,应该是最大状态,缩放因子应该是1.我这里假设在相距一个间距(getIntervalWidth())时,大小变为7/8,当然这个值,大家都可以随意定

    所以(0,1)、(1,7/8)这两个点就形成了一条直线(两点连成一条线),现在是要利用三角形相似,求出来这条直线的公式。

    在这里插入图片描述

    这里根据三角形相似求出来公式倒是难度不大,但需要注意的是,x轴上的单位是getIntervalWidth(),所以在x轴上1实际代表的是1*getIntervalWidth();

    公式求出来以后,就是输入X值,得到对应的缩放因子。那值要怎么得到呢?

    我们知道X的意思是当前item与startX的间距。当间距是0时,得到1。所以x值是:rect.left - mSumDx - mStartX

    其中rect.left - mSumDx表示的是当前item在屏幕上位置。所以rect.left - mSumDx - mStartX表示的是当前item在屏幕上与mStartX的距离。

    这样,缩放系数的计算原理就讲完了,当然大家也可以使用其它的缩放公式,而且也并不一定是用直线,也可以用曲线,无论用什么公式,但一定要保证是线,不能断,一旦出现断裂的情况,就会导致缩放不顺畅,会出现突然变大或者突然变小的情况。现在,大家就可以根据自己的知识储备自由发挥了。

    5.1.8 bug修复

    这里看似效果效果实现的非常完美,但是,当你滑动到底的时候,问题来了:

    在这里插入图片描述

    从图中可以看到,在滑动到底的时候,停留在了倒数第二个Item被选中的状态,应该让最后一个item被选中,才是真正的到底。那怎么解决呢?

    还记得吗?我们在讲解《自定义LayoutManager》中,在刚写好LinearLayoutManager时,到顶和到底后都是可以继续上滑和下滑的。我们为了到顶和到底时,不让它继续滑动,特地添加了边界判断:

    public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
        int travel = dx;
        //如果滑动到最顶部
        if (mSumDx + dx < 0) {
            travel = -mSumDx;
        } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
            //如果滑动到最底部
            travel = mTotalWidth - getHorizontalSpace() - mSumDx;
        }
    	…………
    }
    

    很明显,正是到底的时候,我们添加了判断,让它停留在了最后一个Item在边界的状态。所以,在这里,我们需要对到底判断加以调整,让它可滑动到最后一个item被选中的状态为止。

    首先,我们需要求出来最长能滚动的距离,因为每个item之间的间距是getIntervalWidth(),当一个item滚动距离超过getIntervalWidth()时,就会切换到下一个item被选中,所以一个item最长的滚动距离其实是getIntervalWidth(),所以最大的滚动距离是:

    private int getMaxOffset() {
        return (getItemCount() - 1) * getIntervalWidth();
    }
    

    同样,我们使用在《自定义LayoutManager》中计算较正travel的方法:

    travel + mSumDx = getMaxOffset();
    => travel = getMaxOffset() - mSumDx;

    所以,我们把边界判断的代码改为:

    public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
        int travel = dx;
        //如果滑动到最顶部
        if (mSumDx + dx < 0) {
            travel = -mSumDx;
        } else  if (mSumDx + dx > getMaxOffset()) {
            //如果滑动到最底部
            travel = getMaxOffset()  - mSumDx;
        }
    	…………
    }
    

    现在修复了以后,到底之后就正常了,效果如下图所示:

    在这里插入图片描述

    5.1.9 拓展1:fling校正

    5.1.9.1校正fling原理

    其实,到这里,本节就已经结束了,但在list列表中有一个很常用的需求,但并不好实现,这里给大家提一下。

    有时候,我们看别人写的list的时候,会发现一个非常有意思的现象,就是无论你怎么滚动,它的item都是能左边界对齐的,如下图所示,这是怎么做的呢?

    在这里插入图片描述

    在使用flling校正时,我们主要是重写RecyclerView中的fling方法:

    public boolean fling(int velocityX, int velocityY){
    	return super.fling(flingX, velocityY);
    }
    

    其中:

    • velocityX是横向滑动系数,系统正是根据这个系数计算出应该滑动的距离是多少,velocityX大于0时表示向右滚动,小于0时表示向左滚动
    • velocityY是竖向滑动系数,系统会根据这个系统计算出竖向滑动的距离,velocityY大于0时表示向右滚动,小于0时表示向左滚动

    因为我们的RecyclerView只是单向滑动,所以一般而言,我们只处理一个系数即可。在这里,我们是横向滑动,所以只需要关注velocityX就行。

    一种非常简单的将滑动变得阻塞的方法就是给velocityX乘以一个小数:

    public boolean fling(int velocityX, int velocityY) {
        //缩小滚动距离
        int flingX = (int) (velocityX * 0.40f);
    	return super.fling(flingX, velocityY);
    }
    

    这样就会使本来的滑动距离缩小,使用户感觉到不是那么灵敏,在适配一些高精度手机时经常会使用。

    但,很明显,我们这里需要做不单单是缩小系数这个简单,而是需要更改滑动距离的,以使修正后的滑动距离刚好使item滚动到边界位置。

    所以为了处理滚动距离,我们至少需要三步:

    1.通过velocityX得到实际滚动距离distance
    2.对distance进行校正,得到newDistance
    3.通过newDistance计算得到新的滚动系数fixVelocityX

    5.1.9.2 fling校正代码实现

    这里有几个工具方法,可以满足步骤1和步骤3:

    /**
     * 根据松手后的滑动速度计算出fling的距离
     *
     * @param velocity
     * @return
     */
    private double getSplineFlingDistance(int velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * getPhysicalCoeff() * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }
    
    /**
     * 根据距离计算出速度
     *
     * @param distance
     * @return
     */
    private int getVelocity(double distance) {
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        double aecel = Math.log(distance / (mFlingFriction * mPhysicalCoeff)) * decelMinusOne / DECELERATION_RATE;
        return Math.abs((int) (Math.exp(aecel) * (mFlingFriction * mPhysicalCoeff) / INFLEXION));
    }
    
    /**
     * --------------fling辅助类---------------
     */
    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
    private float mFlingFriction = ViewConfiguration.getScrollFriction();
    private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
    private float mPhysicalCoeff = 0;
    
    private double getSplineDeceleration(int velocity) {
        final float ppi = this.getResources().getDisplayMetrics().density * 160.0f;
        float mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
                * 39.37f // inch/meter
                * ppi
                * 0.84f; // look and feel tuning
    
    
        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    }
    
    private float getPhysicalCoeff() {
        if (mPhysicalCoeff == 0) {
            final float ppi = this.getResources().getDisplayMetrics().density * 160.0f;
            mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
                    * 39.37f // inch/meter
                    * ppi
                    * 0.84f; // look and feel tuning
        }
        return mPhysicalCoeff;
    }
    

    大家不用纠结这些函数是怎么算的,我也看不懂……,这些函数是我在OverScroller.java里抽出来的,这里最核心的是下面两个函数:

    1、 double getSplineFlingDistance(int velocity)

    根据滚动系数得到对应的滚动距离;对应我们步骤中的第一步。

    2、int getVelocity(double distance)

    根据滚动距离,反向得出滚动系数,需要注意的是,通过这个函数得到的系统始终是正值,我们需要根据原来velocityX的正负,去调整最终结果的正负。对应我们步骤中的第三步。

    然后,我们来看fling校正的代码实现:

    public boolean fling(int velocityX, int velocityY) {
        //缩小滚动距离
        int flingX = (int) (velocityX * 0.40f);
        CoverFlowLayoutManager manger = getCoverFlowLayout();
        double distance = getSplineFlingDistance(flingX);
        double newDistance = manger.calculateDistance(velocityX,distance);
        int fixVelocityX = getVelocity(newDistance);
        if (velocityX > 0) {
            flingX = fixVelocityX;
        } else {
            flingX = -fixVelocityX;
        }
        return super.fling(flingX, velocityY);
    }
    

    这里主要有几步:

    1.缩小原滚动系数,使滑动变得阻塞,这句代码大家随意,不想加可以去掉

    int flingX = (int) (velocityX * 0.40f);
    

    2.根据滚动系数,得到原始滚动距离:

    double distance = getSplineFlingDistance(flingX);
    

    3.原始距离校正:

    CoverFlowLayoutManager manger = getCoverFlowLayout();
    double newDistance = manger. manger.calculateDistance(velocityX,distance);
    

    4.根据新的距离计算出新的滚动系数

    int fixVelocityX = getVelocity(newDistance);
    

    5.对新的滚动系数进行正负校正

    if (velocityX > 0) {
        flingX = fixVelocityX;
    } else {
        flingX = -fixVelocityX;
    }
    

    6.返回给系统新的滚动系数

    return super.fling(flingX, velocityY);
    

    有了上面的工具类之后,这段代码就比较容易理解,但这里需要我们自己做的就是对原滚动距离进行校正的部分(步骤三),因为滚动的所有代码都是放在CoverFlowLayoutManager中处理的,所以对距离进行校正的代码,我们也放在CoverFloawLayoutManager中处理。

    这里分为向右滚动的校正和向左回滚的校正,我们先看向右滚动的校正方法(即先忽略velocityX<0的情况)。

    (1)、向右滚动校正

    单纯列出向右滚动的代码如下:

    public double calculateDistance(int velocityX,double distance) {
        int extra = mSumDx % getIntervalWidth();
        double realDistance;
        if (distance < getIntervalWidth()) {
            realDistance = getIntervalWidth() - extra;
        }else {
            realDistance = distance - distance % getIntervalWidth() - extra;
        }
        return realDistance;
    }
    

    这段校正的代码总共分为三步:

    第一步:计算滚动余额

    因为我们需要对distance进行校正,在校正之前,我们知道已经滚动了mSumDx的距离了。但这个距离并不一定是当前Item刚好被选中的初始态(也就是item最大的状态)。我们知道只我们滚动的距离是getIntervalWidth()的倍数时,当前被选中的item也是最大状态。所以,我们用mSumDx % getIntervalWidth()就可以得出当前滚动距离超出item刚好被选中的初始状态多少了。

    如下图所示:

    在这里插入图片描述

    我们知道,我们需要让item的滚动距离正好是getIntervalWidth()的整数倍,才可以让当前选中的item刚好最大。也就是说,我们需要再滚动getIntervalWidth()-extra的距离。

    所以,下面分两种情况进行校正:

    1、如果当前的滚动距离太小,还没有getIntervalWidth()长,那么就直接让它滚动getIntervalWidth()-extra即可

    realDistance = getIntervalWidth() - extra;
    

    2、那如果distance比较长呢?我们需要先对distance进行裁剪,让distance刚好是getIntervalWidth()的整数倍。我们通过distance - distance % getIntervalWidth()将distance进行裁剪,将多余的部分减掉。然后再加上余数即可

    所以它的校正值应该是:

    realDistance = distance - distance % getIntervalWidth() + getIntervalWidth() - extra;
    

    在这里,我做了个简化,因为校正后的distance已经是getIntervalWidth的倍数了,所以在extra中的getIntervalWidth()是可以不加的。所以简化后的距离校正就变为:

    realDistance = distance - distance % getIntervalWidth() - extra;
    

    这也就是上面代码中的公式了。

    到这里,向右滚动的距离校正的部分就讲完了,下面来看看效果吧

    在这里插入图片描述
    可以看到,在向右滚动时,每次都能正好停留在当前Item被选中的初始位置。但如果我们向左回滚的话,问题就出现了:

    在这里插入图片描述
    可以看到,在向左滚动后,并不是每次都会停在Item被选中的初始位置。像动画中的在滚到第23个item时,很明显第23个item并不是选中的初始状态,它的大小与第24个item差不多。所以我们就来看看在向左滚动时,如何进行校正,与向右滚动的校正方法又有什么不同。

    (2)、向左回滚校正

    首先,大家需要了解一个事情,这里虽然是向左滚动,但如果我们使用int extra = mSumDx % getIntervalWidth();得到的extra还是下图中的“超出距离”部分吗?

    在这里插入图片描述
    答案是是的,要理解回滚的“超出距离”部分,需要知道mSumDx的值是什么?

    如上图所标记的,mSumDx是从中心点到第一个item的左上角的滚动距离总和,也就是第一个Item在初始化的位置到现在的位置和距离,就是mSumDx。所以用mSumDx % getIntervalWidth()表示的就是当前滚动距离超出多少个一半卡片,所以,int extra = mSumDx % getIntervalWidth();得到的就是上图中的“超出距离”部分。

    但问题来了,现在我们要校正的是向左回滚!如上图的状态,当前是停留在中心点的位置,我们只需要在向左滚动个“超出距离”,就可以滚动到卡片中心点位置了。

    同样,我们要分两种情况进行校正:

    1、如果当前的滚动距离太小,还没有getIntervalWidth()长,那么就直接让它滚动extra即可

    realDistance = extra;
    

    2、那如果distance比较长呢?我们需要先对distance进行裁剪,让distance刚好是getIntervalWidth()的整数倍。我们通过distance - distance % getIntervalWidth()将distance进行裁剪,将多余的部分减掉。然后再加上余数即可

    所以它的校正值应该是:

    realDistance = distance - distance % getIntervalWidth() + extra;
    

    所以,组合起来,向左滚动校正的代码就是:

    public double calculateDistance(int velocityX,double distance) {
        int extra = mSumDx % getIntervalWidth();
        double realDistance;
        if (distance < getIntervalWidth()) {
            realDistance = extra;
        }else {
            realDistance = distance - distance % getIntervalWidth() + extra;
        }
        return realDistance;
    }
    

    然后,我们将向右滚动和向左回滚的代码组合起来,就是完整的校正代码了:

    public double calculateDistance(int velocityX,double distance) {
        int extra = mSumDx % getIntervalWidth();
        double realDistance;
        if (velocityX>0){
            if (distance < getIntervalWidth()) {
                realDistance = getIntervalWidth() - extra;
            }else {
                realDistance = distance - distance % getIntervalWidth() - extra;
            }
        }else {
            if (distance < getIntervalWidth()) {
                realDistance = extra;
            }else {
                realDistance = distance - distance % getIntervalWidth() + extra;
            }
        }
        return realDistance;
    }
    

    现在无论我们向左滚动还是向右滚动,都是可以正好停留在Item被选中的初始位置了。

    5.1.10 拓展2:制作3D画廊的原理

    如果我们想要制作3D画廊要怎么做呢?在VIVO的游戏空间中,3D画廊的效果非常酷:

    在这里插入图片描述
    同样地,我们也只需要计算出旋转度数曲线,然后将值设置给child.setRotationY(float rotation)即可。

    先列出旋转代码:

    private void handleChildView(View child, int moveX) {
        float radio = computeScale(moveX);
        float rotation = computeRotationY(moveX);
    
        child.setScaleX(radio);
        child.setScaleY(radio);
    
        child.setRotationY(rotation);
    }
    

    在这里,我们首先使用float rotation = computeRotationY(moveX);计算出旋转度数,然后利用child.setRotationY(rotation);对View进行旋转。

    最关键的部分是,computeRotationY(moveX)是如何实现的,它的实现代码如下:

    /**
     * 最大Y轴旋转度数
     */
    private float M_MAX_ROTATION_Y = 30.0f;
    private float computeRotationY(int x) {
        float rotationY;
        rotationY = -M_MAX_ROTATION_Y * x / getIntervalWidth();
        if (Math.abs(rotationY) > M_MAX_ROTATION_Y) {
            if (rotationY > 0) {
                rotationY = M_MAX_ROTATION_Y;
            } else {
                rotationY = -M_MAX_ROTATION_Y;
            }
        }
        return rotationY;
    }
    

    很明显,这里也是个函数,那我们要怎么计算出这个函数呢?

    首先,我们需要定义一个旋转最大值,不能让角度无限制的旋转,这里定义的最大值是float M_MAX_ROTATION_Y = 30.0f,然后在Item在初始化状态没有移动时,旋转度数是0,假设移动到下一个item后,达到最大旋转度数。

    所以,根据上面的假设,这个缩放直线就可以画出来了:

    在这里插入图片描述
    其中:

    • X轴:与缩放系数曲线中X轴表示的相同,都表示item距离中心点的距离。很明显,单位是getIntervalWidth();
    • Y轴:表示旋转度数。

    所以,根据三角形相似,就很容易得出:

    y = M_MAX_ROTATION_Y * x / getIntervalWidth();

    大家可能会问了,为什么在代码中y = - M_MAX_ROTATION_Y * x / getIntervalWidth(),要加一个负号呢?

    这是因为我们使用的child.setRotationY(rotation);中的setRotationY的旋转效果决定的,大家可以尝试把负号去掉,看下效果,与我们想要的旋转效果刚好是相反的:

    在这里插入图片描述
    所以,我们需要加上负号,让它的旋转效果与我们想要的相同。在计算了rotationY以后,然后需要进行最大值判断,让它最大不能超过旋转最大值:

    if (Math.abs(rotationY) > M_MAX_ROTATION_Y) {
        if (rotationY > 0) {
            rotationY = M_MAX_ROTATION_Y;
        } else {
            rotationY = -M_MAX_ROTATION_Y;
        }
    }
    

    到这里,3D旋转画廊就实现出来了,效果如下图所示:

    在这里插入图片描述

    这里,有关3D画廊的部分就全部讲完了,本节中主要讲解了如何利用自定义LayoutManager实现3D画廊的功能,虽然实现了效果,但这也只是一个demo,或多或少地会存在一些问题,如果用于实战中,还是很多不足的,主要是为了给大家讲解实现原理。在github上,有一个利用RecyclerView实现画廊的工程,我的工程也参考了它的代码,能够直接用于项目中,工程地址:https://github.com/ChenLittlePing/RecyclerCoverFlow


    如果我的文章对你有帮助的话,你应该也会喜欢我的公众号
    在这里插入图片描述


    如果本文有帮到你,记得加关注哦
    CSDN源码现在不能零分下载了,必须强制最低一分,我设置为了最低分,如果没分的同学,可以从github上下载。
    源码地址:https://download.csdn.net/download/harvic880925/10933790
    github代码地址:https://github.com/harvic/harvic_blg_share 位于RecylcerView(六)
    转载请标明出处,https://blog.csdn.net/harvic880925/article/details/86606873 谢谢

    展开全文
  • 1 理解计算机是怎么运行程序的 2 运行一个已解释的程序 3 运行一个已编译的程序 4 C++在哪里 5 理解Visual c++中的程序文件 6 创建源代码文件 7 理解并创建头文件 第二章 结构和语法 8 理解计算机语言 9 理解计算机...
  • 1 理解计算机是怎么运行程序的 2 运行一个已解释的程序 3 运行一个已编译的程序 4 C++在哪里 5 理解Visual c++中的程序文件 6 创建源代码文件 7 理解并创建头文件 第二章 结构和语法 8 理解计算机语言 9 理解计算机...
  • 1 理解计算机是怎么运行程序的 2 运行一个已解释的程序 3 运行一个已编译的程序 4 C++在哪里 5 理解Visual c++中的程序文件 6 创建源代码文件 7 理解并创建头文件 第二章 结构和语法 8 理解计算机语言 9 理解计算机...
  • 1 理解计算机是怎么运行程序的 2 运行一个已解释的程序 3 运行一个已编译的程序 4 C++在哪里 5 理解Visual c++中的程序文件 6 创建源代码文件 7 理解并创建头文件 第二章 结构和语法 8 理解计算机语言 9 理解计算机...
  • 但是老师要求我们分开,不是很理解),服务层要操作视图层时是直接把视图传过去还是只传控件,以后要移植的话服务层怎么保证不用改很多地方等问题,以至于我自己独立写程序时很多地方不确定这样写是不是比较科学的。...
  • 在各个章节的文字都排好后,设置第一章的页眉(若连页眉都不知怎么加,请参考Word帮助)。然后跳到第一章的末尾,菜单栏上选“插入︱分隔符”,分节符类型选“下一页”,不要选“连续”(除非你想第二章的标题放在第...
  • Windows知道怎么画你的菜单以及你的标注着“Hello, Cfan!”的按钮。当CFan某个快乐的小编(譬如:小飞)点击这个按钮的时候,Windows也明白按钮按下去的时候该有的模样,甚至,当这个友好的按钮获取焦点时,Windows也...
  • Android 上百实例源码分析以及开源分析 集合打包4

    千次下载 热门讨论 2012-07-10 21:54:03
    在Jamendo中,主要是通过再定义一个SeparatedListAdapter来进行这个工作,我们来看看它是怎么实现的:我理解的Adapter过程,首先通过调用getCount()来获得总Row数目,然后对一行调用getView进行绘制,因此要实现在...
  • 软件工程教程

    热门讨论 2012-07-06 23:10:29
    用例只描述参与者和系统在交互过程中做些什么,并不描述怎么做。 用例图 关联关系 用例图 泛化关系 用例图 泛化关系 用例图 用例图 用例图 用例用于什么情况? 不知道什么情况不用用例 如果没有用到用例,...
  • asp.net知识库

    2015-06-18 08:45:45
    怎么在ASP.NET 2.0中使用Membership asp.net 2.0-实现数据访问(1) ASP.NET 2.0 新特性 .NET 2.0里使用强类型数据创建多层应用 在MastPage中引用脚本资源 2.0正式版中callback的一些变化+使用示例(ASP.NET 2.0)...
  • MAPGIS地质制图工具

    2013-05-06 16:15:30
    在编辑视图中,如果状态为准备状态,按住Ctrl右击,将弹出常用图元编辑快捷菜单;按住Shift右击,将弹出扩展功能快捷菜单。 编辑本段安装与卸载 系统要求 基础软件:MapGis软件。推荐MapGis 6.7版本Build051118。 ...
  • vc++ 应用源码包_1

    热门讨论 2012-09-15 14:22:12
    使用了个类五个模块类演示了atl的调用方法 autoplaysnd mp3 播放器源码 重载了自带的控件进行播放 aviplayer avi播放器源码 引用了atl控件播放 beautifulskin 源码 演示了各种控件方法 Browser.Net源码 C#的一...
  • vc++ 应用源码包_2

    热门讨论 2012-09-15 14:27:40
    使用了个类五个模块类演示了atl的调用方法 autoplaysnd mp3 播放器源码 重载了自带的控件进行播放 aviplayer avi播放器源码 引用了atl控件播放 beautifulskin 源码 演示了各种控件方法 Browser.Net源码 C#的一...
  • vc++ 应用源码包_6

    热门讨论 2012-09-15 14:59:46
    使用了个类五个模块类演示了atl的调用方法 autoplaysnd mp3 播放器源码 重载了自带的控件进行播放 aviplayer avi播放器源码 引用了atl控件播放 beautifulskin 源码 演示了各种控件方法 Browser.Net源码 C#的一...
  • vc++ 应用源码包_5

    热门讨论 2012-09-15 14:45:16
    使用了个类五个模块类演示了atl的调用方法 autoplaysnd mp3 播放器源码 重载了自带的控件进行播放 aviplayer avi播放器源码 引用了atl控件播放 beautifulskin 源码 演示了各种控件方法 Browser.Net源码 C#的一...
  • vc++ 应用源码包_4

    热门讨论 2012-09-15 14:38:35
    使用了个类五个模块类演示了atl的调用方法 autoplaysnd mp3 播放器源码 重载了自带的控件进行播放 aviplayer avi播放器源码 引用了atl控件播放 beautifulskin 源码 演示了各种控件方法 Browser.Net源码 C#的一...
  • vc++ 应用源码包_3

    热门讨论 2012-09-15 14:33:15
    使用了个类五个模块类演示了atl的调用方法 autoplaysnd mp3 播放器源码 重载了自带的控件进行播放 aviplayer avi播放器源码 引用了atl控件播放 beautifulskin 源码 演示了各种控件方法 Browser.Net源码 C#的一...
  • 答:软件生命期瀑布模型分为个阶段: ● 可行性研究与计划(确定系统的目标和规模,分析项目的可行性); ● 需求分析与规格说明(明确系统的规格和要求); ● 设计(包括概要设计和详细设计,将系统分解为模块);...
  • excel的使用

    2012-11-25 17:06:01
    在单元格中显示公式如果工作表中的数据多数是由公式生成的,想要快速知道每个单元格中的公式形式,以便编辑修改,可以这样做:用鼠标左键单击“工具”菜单,选取“选项”命令,出现“选项”对话框,单击“视图”选项...
  • vc++ 开发实例源码包

    2014-12-16 11:25:17
    使用了个类五个模块类演示了atl的调用方法 class CDHtmlSinkHandler; // Events Sink Base class CDHtmlEventSink; // IHTMLDocument2 Events Sink // IDispatch class CDHtmlControlSink; // ActiveX Control ...

空空如也

空空如也

1 2
收藏数 24
精华内容 9
关键字:

六视图怎么画