手摸手教你写炫酷控件 (仿即刻首页垂直滚动图)

Android · jeasonwong · 于 发布 · 最后由 roxas回复 · 2553 次阅读
1405

项目地址:https://github.com/JeasonWong/JikeGallery

话不多说,先上效果。

图二福利

这个效果是在即刻app上看到,觉得很不错,遂仿之。

先说下我的实现思路(以上方的图片滚动为例,下方的文字实现效果类似):

  • 自定义ViewGroup
  • 装载两个ImageView和一个阴影View
  • 通过一定规律交替控制两个ImageView和它们的marginTop,在onLayout()中实现
  • marginTop的具体值由属性动画控制,不断调用requestLayout()

接下来依次说明

###一、自定义ViewGroup

    //滑动状态
    protected static final int STATUS_SMOOTHING = 0;
    //停止状态
    protected static final int STATUS_STOP = 1;

    //ViewGroup宽高
    protected int mWidth, mHeight;
    //变化的marginTop值
    protected int mSmoothMarginTop;
    //默认状态
    protected int mStatus = STATUS_STOP;
    //滚动时间间隔
    protected int mDuration = 500;
    //重复次数
    protected int mRepeatTimes = 0;

    ...

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mSmoothMarginTop = -h;
        initView();
    }

    protected abstract void initView();

    ...

    /**
     * 是否是奇数圈
     *
     * @return 结果
     */
    protected boolean isOddCircle() {
        return mRepeatTimes % 2 == 1;
    }

先了解下成员变量,其中最重要的一个就是mSmoothMarginTop,相信很多人都知道一个View的marginTop可以设为负数,这个负数可以给我们带来太多的方便。

pic0

上图的图0就是我们展现在屏幕上的ImageView,图1则是屏幕外marginTop为-height的ImageView,这个一定要明白,接下来才好继续实现

###二、装载两个ImageView和一个阴影View

    private List<String> mImgList = new ArrayList<>();
    private ImageView[] mImgs = new ImageView[2];
    private View mShadowView;

    ...

    @Override
    protected void initView() {

        //如果没有内容,则不进行初始化操作
        if (mImgList.size() == 0) {
            return;
        }

        removeAllViews();

        MarginLayoutParams params = new MarginLayoutParams(mWidth, mHeight);

        //两个ImageView加载前两张图
        for (int i = 0; i < mImgs.length; i++) {
            mImgs[i] = new ImageView(getContext());
            addViewInLayout(mImgs[i], -1, params, true);
            Glide.with(getContext()).load(getImgPath(i)).centerCrop().into(mImgs[i]);
        }

        //创建阴影View
        mShadowView = new View(getContext());
        mShadowView.setBackgroundColor(Color.parseColor("#60000000"));
        mShadowView.setAlpha(0);
        addViewInLayout(mShadowView, -1, params, true);
    }

    ...

    /**
     * 获取图片地址
     * 
     * @param position 位置
     * @return  图片地址
     */
    private String getImgPath(int position) {
        position = position % mImgList.size();
        return mImgList.get(position);
    }    

关键点说明:

  • MarginLayoutParams 为了之后方便取出margin值
  • addViewInLayout() 为了对requestLayout的绝对控制
  • getImgPath() 为了实现循环滚动

这样一来,我们需要的View都已经创建好了。

###三、通过一定规律交替控制两个ImageView和它们的marginTop,在onLayout()中实现

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int cCount = getChildCount();
        MarginLayoutParams cParams;

        for (int i = 0; i < cCount; i++) {
            View childView = getChildAt(i);
            cParams = (MarginLayoutParams) childView.getLayoutParams();

            int cl = 0, ct = 0, cr, cb;

            if (isOddCircle()) {
                if (i == 1) {
                    cl = cParams.leftMargin;
                    ct = mSmoothMarginTop + mHeight;
                } else if (i == 0) {
                    cl = cParams.leftMargin;
                    ct = mSmoothMarginTop;
                }
            } else {
                if (i == 0) {
                    cl = cParams.leftMargin;
                    ct = mSmoothMarginTop + mHeight;
                } else if (i == 1) {
                    cl = cParams.leftMargin;
                    ct = mSmoothMarginTop;
                }
            }
            //控制shadowView
            if (i == 2) {
                cl = cParams.leftMargin;
                ct = mSmoothMarginTop + mHeight;
            }

            cr = cl + mWidth;
            cb = ct + mHeight;
            childView.layout(cl, ct, cr, cb);
        }

    }

以上实现的就是不断的替换图1和图2谁上谁下,阴影和下方的图保持同步。

###四、marginTop的具体值由属性动画控制,不断调用requestLayout()

先看基类ViewGroup

    /**
     * 开启滑动
     *
     */
    public void startSmooth() {

        if (mStatus != STATUS_STOP) {
            return;
        }

        ValueAnimator animator = ValueAnimator.ofFloat(-mHeight, 0);
        animator.setDuration(mDuration);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                float marginTop = (float) animation.getAnimatedValue();
                mSmoothMarginTop = (int) marginTop;

                if (marginTop == 0) {

                    postDelayed(new Runnable() {
                        @Override
                        public void run() {

                            mRepeatTimes++;

                            mSmoothMarginTop = -mHeight;

                            doAnimFinish();

                            mStatus = STATUS_STOP;

                        }
                    }, 50);

                } else {
                    doAnim();
                }
            }
        });
        animator.start();
        mStatus = STATUS_SMOOTHING;
    }

    //动画结束
    protected abstract void doAnimFinish();

    //动画进行时
    protected abstract void doAnim();

关键点说明:

  • 属性动画控制着mSmoothMarginTop在[-mHeight, 0]中变化
  • 每完成一圈,mRepeatTimes自增1

再来看看Gallery实现类

    @Override
    protected void doAnimFinish() {
        if (isOddCircle()) {
            Glide.with(getContext()).load(getImgPath(mRepeatTimes + 1)).centerCrop().into(mImgs[0]);
        } else {
            Glide.with(getContext()).load(getImgPath(mRepeatTimes + 1)).centerCrop().into(mImgs[1]);
        }
        mShadowView.setAlpha(0);
    }

    @Override
    protected void doAnim() {
        mShadowView.setAlpha(((1 - (-mSmoothMarginTop) / (float) mHeight)));
        requestLayout();
    }

关键点说明:

  • 通过mSmoothMarginTop与mHeight的比值控制阴影View的透明度
  • 每次动画完成时,下方的图(此时下方的图已经超出屏幕了,而上方图显示在屏幕内)需要加载第三张图,使用getImgPath()取出

pic1

以上就是图片的滚动实现,文字的滚动90%是一样的,有点区别的就是需要文字需要控制下垂直居中,我就不赘述了

如果有更好的思路,欢迎交流,开源本身就是大家互相喷喷,互相进步嘛。

本帖已被设为精华帖!
共收到 7 条回复
1430
roxas · #1 ·

有趣的控件,喜欢作者一言不合就自己实现的风格。
估计这个项目再深入一点就该研究recyclerview,listview,viewpager等等了。
数据(图片地址)的绑定在控件里实现了,所以引入了glide。也许别人不喜欢glide而是picasso呢?所以数据绑定得独立出来。

1405

#1楼 @roxas 嗯嗯,其实本就想说明下自己对这个控件的思路~ 实现方式肯定很多种。关于Glide就是自己偷懒了~ 哈哈,如果真有很多朋友需要使用到,我还是很乐意这里抽离出来的~

96
331442092 · #3 ·

很不错,学习了

30
d_clock · #4 ·

可以啊,很强势!已入选今天的《Diycode每日精选》!

1405

#4楼 @d_clock 好开心~~

96
yzhliang · #6 ·

棒棒的

1430

#2楼 @jeasonwong 多嘴几句,我觉得不用等到“很多朋友需要使用时”再优化。提高对自己的要求,当你把抽象变成一种习惯的时候,写到那你就会觉得不爽。说出:“我一定要重构!”,不是因为外界的压力譬如“服务器怎么又down了!?”,“APP怎么又崩了”,“wtf,这什么鬼库,一点也不好用”等等,没压力就没办法成长,这太被动了。你现在喜欢自己临摹,比那些只会找现成的好多了,继续提高吧,早日成为大神。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册