Android初级进阶之自定义果冻视图(BouncingJellyView)(一)

简介: 前言上一个周没有写博客,是我太懒,无法坚持。在上一个周,除去工作的任务(迭代版本,修复BUG)之外,我一直在模仿一个UI效果。我呢,算是一个米粉,我前面的博客,有一些效果就是来自MIUI。

前言

上一个周没有写博客,是我太懒,无法坚持。在上一个周,除去工作的任务(迭代版本,修复BUG)之外,我一直在模仿一个UI效果。我呢,算是一个米粉,我前面的博客,有一些效果就是来自MIUI。在MIUI中,很多的列表都具有弹性和粘性,个人觉得这个效果不错,于是就模仿了一下。

本来开始之初是为了将这个效果封装成为一个单独的UI控件,结果写着写着就发现这样是不合理的,于是就放在一旁等待解决方案,先看看实际的效果吧。(前面两个是我项目中的实际效果)

img_7e91cef9ac6911e9f203f2b75af924dd.gif
订单详情效果
img_b94715dcd6a09bbace81a75374e3d0c9.gif
设置列表效果
img_91e1c02e422df4faf284b7902022fe4f.gif
demo->RecyclerView效果

怎么样,看上去效果还是挺可以的吧,不得不说,MIUI在一些小细节上面做得非常不错,很多效果都值得我们深入的进行学习。

注意

  1. 本博客最主要的是为了向大家展示一种解决思路,文章中的类表效果用到实际项目中还是有些许问题。
  2. 为了方便起见,本文中使用的动画效果来自于JakeWharton大神的NineOldAndroids的支持库,个人非常不建议新手直接就来使用开源库,最起码应该熟悉一些基础。

分析

刚开始的时候,我一直在网上找类似的效果,一直是没有找到。直到我学习完属性动画之后才发现,其实这个效果实现起来是非常的简单。

  1. 整个效果看上去分为上拉和下来,上拉和下拉的时候进行缩放。

     1. 下拉:
         将View的中心点移到(width/2,0)中进行Scale缩放
     2. 上拉
         将View的中心点移到(width/2,height)中进行Scale缩放
    
  2. 松手之后会有一个回弹效果,使用ValueAnimator来进行散发scale值,采用OvershootInterpolator差值器就能达到这样的效果。

编码

1. 选择继承

自定义View又几种方式:

  1. 继承自View实现效果。
  2. 继承原生控件进行拓展。
  3. 组合控件。

很明显,效果图中都是包含了子控件的,可以选择继承ViewGroup来实现,但是我根本不关心子控件的一些测量和layout,所以需要继承已经实现的ViewGroup。最后我选定的是使用ScrollView,原因是为了兼容滚动,并且需要监听是否已经滚动到了底部。

2.准备工作

  1. 创建项目
  2. 引用开源库 compile 'com.nineoldandroids:library:2.4.0'
  3. 创建自定义控件类继承ScrollView,实现三个构造方法,并且在xml中引用

3. 自定义属性

首先思考我们需要哪一些属性,比方说手指抬起后回弹的速度,回弹的效果方式(其实就是不同的差值器),能够进行果冻缩放的方式,只能是顶部、底部或者不限制。

在value文件夹中创建attr.xml

    
    <attr name="BouncingDuration" format="integer" />
    <attr name="BouncingInterpolator" format="enum">
        <enum name="OvershootInterpolator" value="1" />
        <enum name="BounceInterpolator" value="2" />
        <enum name="LinearInterpolator" value="3" />
        <enum name="AccelerateDecelerateInterpolator" value="4" />
    </attr>
    <attr name="BouncingType" format="enum">
        <enum name="none" value="0" />
        <enum name="top" value="1" />
        <enum name="bottom" value="2" />
        <enum name="both" value="3" />
    </attr>

    <declare-styleable name="BouncingJellyScrollView">
        <attr name="BouncingDuration" />
        <attr name="BouncingInterpolator" />
        <attr name="BouncingType" />
    </declare-styleable>

将attr独立出来的原因是我还有几个控件需要使用相同的一些属性。

5. 初始化

在构造方法中初始化一些常量值和属性。

其它的一些工具类方法


    public class BouncingType {
        public static final int NONE = 0;
        public static final int TOP = 1;
        public static final int BOTTOM = 2;
        public static final int BOTH = 3;
    }

    public class BouncingInterpolatorType {

        public static final int OVERSHOOT_INTERPOLATOR = 1;
        public static final int BOUNCE_INTERPOLATOR = 2;
        public static final int LINEAR_INTERPOLATOR = 3;
        public static final int ACCELERATE_DECELERATE_INTERPOLATOR = 4;
    
        /**
         * 获取弹跳类型
         *
         * @return
         */
        public static TimeInterpolator getTimeInterpolator(int type) {
            TimeInterpolator mTimeInterpolator = null;
            switch (type) {
                case OVERSHOOT_INTERPOLATOR:
                    mTimeInterpolator = new OvershootInterpolator();
                    break;
                case BOUNCE_INTERPOLATOR:
                    mTimeInterpolator = new BounceInterpolator();
                    break;
                case LINEAR_INTERPOLATOR:
                    mTimeInterpolator = new LinearInterpolator();
                    break;
                case ACCELERATE_DECELERATE_INTERPOLATOR:
                    mTimeInterpolator = new AccelerateDecelerateInterpolator();
                    break;
            }
            return mTimeInterpolator;
        }
    }

初始化属性

    
    /**
     * @param attrs
     */
    private void initAttr(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.BouncingJellyScrollView);
            //差值器
            mTimeInterpolator = BouncingInterpolatorType.getTimeInterpolator(typedArray.getInteger(
                    R.styleable.BouncingJellyScrollView_BouncingInterpolator
                    , BouncingInterpolatorType.OVERSHOOT_INTERPOLATOR));
            //回弹速度
            mBouncingDuration = typedArray.getInteger(R.styleable.BouncingJellyScrollView_BouncingDuration, mBouncingDuration);
            //果冻类型
            mBouncingType = typedArray.getInt(R.styleable.BouncingJellyScrollView_BouncingType, BouncingType.BOTH);
            typedArray.recycle();
            //获取是差值  整个屏幕的三倍大小
            bouncingOffset=ScreenUtils.getScreenHeight(getContext()) * 3;
        }
    }

onSizeChanged中验证模式

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //判断可滚动的内容是不是小于整个屏幕的高度,以防底部进行所动
    int contentHeight = getChildAt(0).getHeight();
    if (contentHeight > 0 && contentHeight <= ScreenUtils.getScreenHeight(getContext())) {
        mBouncingType = BouncingType.TOP;
    }
}

4. 开始编码

因为我们继承的是ViewGroup,子View还需要拦截事件,所以我们需要重写dispatchTouchEvent方法,并且在其中拦截事件分发和做缩放效果。

先实现在顶部进行滑动的时候随着手指移动而进行缩放


    /**
     * 从顶部开始滑动
     */
    public void bouncingTo() {
        //设置X坐标点
        ViewHelper.setPivotX(this, getWidth() / 2);
        //设置Y坐标点
        ViewHelper.setPivotY(this, 0);
        //进行缩放
        ViewHelper.setScaleY(this, 1.0f + offsetScale);      
    }
    
     /**
     * 从顶部开始滑动
     */
    public void bouncingBottom() {
        //设置X坐标点
        ViewHelper.setPivotX(this, getWidth() / 2);
        //设置Y坐标点
        ViewHelper.setPivotY(this, getHeight());
        ViewHelper.setScaleY(this, 1.0f + offsetScale);
    }

在ACTION_DOWN记录按下的坐标,用于计算缩放值和进行回弹。因为ACTION_DOWN事件必定会传递到子view的,所以不能直接返回true。

//移动坐标
dowY = (int) event.getRawY();
//按下坐标 用于计算缩放值
dowY2 = (int) event.getRawY();

在ACTION_MOVE中进行事件分发和缩放。

  1. 实现顶部滑动缩放,主要原理是判断当前是不是滚动到了顶部,获取手指移动的方向和距离。

    moveX = (int) event.getRawX();
    moveY = (int) event.getRawY();
    //dy值 判断方向
    int dy = moveY - dowY;
    dowY = moveY;
    //顶部
    if (dy > 0 && getScrollY() == 0) {
    //判断果冻的类型
    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
    //获取现在坐标与按下坐标的差值
    int abs = moveY - dowY2;
    //计算缩放值
    offsetScale = (Math.abs(abs) / bouncingOffset);
    if (offsetScale > 0.3f) {
    offsetScale = 0.3f;
    }
    isTop = true;
    bouncingTo();
    return true;
    }
    }

实现第一步效果如下:

img_7eb02140b89b862a4cfb93e42836f711.gif
顶部下拉缩放1
  1. 回拉恢复

效果是出来了,从顶部下拉的时候慢慢的缩放了,但是如果在下拉一定距离后上拉会是怎么样的呢?应该是慢慢的缩回去,然后再进行滚动。 需要在顶部if后面再加上判断

 if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//为顶部 并且dy为下拉 并且缩放值大于0
    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
        //获取现在坐标与按下坐标的差值
        int abs = moveY - dowY2;
        //计算缩放值
        offsetScale = (Math.abs(abs) / bouncingOffset);
        if (offsetScale > 0.3f) {
            offsetScale = 0.3f;
        }
        if (abs <= 0) {
            offsetScale = 0;
            dowY2 = moveY;
        }
        isTop = true;
        bouncingTo();
        return true;
    }
}

效果如下:

img_0f67680cd5e5db11d9fa0a194bce7154.gif
顶部下拉缩放回拉恢复
  1. 手指抬起进行回弹

前面两步完成了整个拉取的过程,现在只要加上手机抬起的时候进行回弹就可以了。整个回弹过程是有一个时间段,并且还有一个效果。采用ValueAnimator来散发offsetScale值来不断的改变缩放值就能达到效果。

ACTION_UP代码

 if (mBouncingType != BouncingType.NONE) {
        if (offsetScale > 0) {
            backBouncing(offsetScale, 0);
            return true;
        }
    }

/**
 * 进行回弹
 *
 * @param from
 * @param to
 */
private void backBouncing(final float from, final float to) {
    //初始化
    if (animator != null && animator.isRunning()) {
        animator.cancel();
        animator = null;
        offsetScale = 0;
        bouncingTo();
    }
    if (mTimeInterpolator == null) {
        mTimeInterpolator = new OvershootInterpolator();
    }
    //散发值
    animator = ValueAnimator.ofFloat(from, to).setDuration(mBouncingDuration);
    animator.setInterpolator(mTimeInterpolator);//差值器
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //获取动画阶段的值
            offsetScale = (float) animation.getAnimatedValue();
            if (isTop) {//回弹到顶部
                bouncingTo();
            } else {//回弹到底部
                bouncingBottom();
            }
        }
    });
    animator.start();
}

效果如下:

img_16bb3ed6dac32fb42901a1e0ded717c5.gif
顶部下拉缩放抬起回弹

其实到这里,整个果冻视图就已经算是完成了,至于底部滑动,缩放都是一样的,只是方向,值相反而已。判断是否已经滚动到了底部,判断方向等。以下附上dispatchTouchEvent的代码,代码量有些冗余,只是为了每个部分的清晰而已。

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //移动坐标
            dowY = (int) event.getRawY();
            //按下坐标 用于计算缩放值
            dowY2 = (int) event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            if (mBouncingType != BouncingType.NONE) {
                moveX = (int) event.getRawX();
                moveY = (int) event.getRawY();
                //dy值 判断方向
                int dy = moveY - dowY;
                dowY = moveY;
                //顶部
                if (dy > 0 && getScrollY() == 0) {
                    //判断果冻的类型
                    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
                        //获取现在坐标与按下坐标的差值
                        int abs = moveY - dowY2;
                        //计算缩放值
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        isTop = true;
                        bouncingTo();
                        return true;
                    }
                } else if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//为顶部 并且dy为下拉 并且缩放值大于0
                    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
                        //获取现在坐标与按下坐标的差值
                        int abs = moveY - dowY2;
                        //计算缩放值
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        if (abs <= 0) {
                            offsetScale = 0;
                            dowY2 = moveY;
                        }
                        isTop = true;
                        bouncingTo();
                        return true;
                    }
                }

                //底部
                if (dy < 0 && getScrollY() + getHeight() >= computeVerticalScrollRange()) {//滚动到底部
                    if (mBouncingType == BouncingType.BOTTOM || mBouncingType == BouncingType.BOTH) {
                        int abs = moveY - dowY2;
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        isTop = false;
                        bouncingBottom();
                    }
                } else if (dy > 0 && getScrollY() + getHeight() >= computeVerticalScrollRange() && offsetScale > 0) {
                    if (mBouncingType == BouncingType.BOTTOM || mBouncingType == BouncingType.BOTH) {
                        int abs = moveY - dowY2;
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        if (abs >= 0) {
                            offsetScale = 0;
                            dowY2 = moveY;
                        }
                        isTop = false;
                        bouncingBottom();
                        return true;
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mBouncingType != BouncingType.NONE) {
                if (offsetScale > 0) {
                    backBouncing(offsetScale, 0);
                    return true;
                }
            }
            break;
    }
    return super.dispatchTouchEvent(event);
}

来一个整体完成的效果图:

img_5d74617175d566afd0fc3bff94e00677.gif
顶部下拉缩放完整

其它View

  1. RecyclerView,ListVIew实现的原理都是一样的,判断是否在顶部,滑动方向等,再进行缩放即可。另外我实现了一个RecycerView的demo,代码和上面的基本上一致。

最后

源码地址

最后,这暂时是一个最基本的,等有时间我会继续完成这个自定义View的。如果可能,希望大家能够给我一个star。

目录
相关文章
|
1月前
|
Android开发 开发者
安卓应用开发中的自定义视图
【9月更文挑战第37天】在安卓开发的海洋中,自定义视图犹如一座座小岛,等待着勇敢的探索者去发现其独特之处。本文将带领你踏上这段旅程,从浅滩走向深海,逐步揭开自定义视图的神秘面纱。
41 3
|
1月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
86 0
|
20天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
22天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
30 5
|
1月前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
2月前
|
Android开发 开发者
安卓开发中的自定义视图:从入门到精通
【9月更文挑战第19天】在安卓开发的广阔天地中,自定义视图是一块充满魔力的土地。它不仅仅是代码的堆砌,更是艺术与科技的完美结合。通过掌握自定义视图,开发者能够打破常规,创造出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战应用,一步步展示如何用代码绘出心中的蓝图。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往创意和效率的大门。让我们一起探索自定义视图的秘密,将你的应用打造成一件艺术品吧!
62 10
|
2月前
|
XML 编解码 Android开发
安卓开发中的自定义视图控件
【9月更文挑战第14天】在安卓开发中,自定义视图控件是一种高级技巧,它可以让开发者根据项目需求创建出独特的用户界面元素。本文将通过一个简单示例,引导你了解如何在安卓项目中实现自定义视图控件,包括创建自定义控件类、处理绘制逻辑以及响应用户交互。无论你是初学者还是有经验的开发者,这篇文章都会为你提供有价值的见解和技巧。
47 3
|
2月前
|
前端开发 Android开发 开发者
安卓应用开发中的自定义视图基础
【9月更文挑战第13天】在安卓开发的广阔天地中,自定义视图是一块神奇的画布,它允许开发者将想象力转化为用户界面的创新元素。本文将带你一探究竟,了解如何从零开始构建自定义视图,包括绘图基础、触摸事件处理,以及性能优化的实用技巧。无论你是想提升应用的视觉吸引力,还是追求更流畅的交互体验,这里都有你需要的金钥匙。
|
缓存 测试技术 Android开发
|
缓存 测试技术 Android开发
下一篇
无影云桌面