自定义behavior 仿支付宝首页

相信大家平时使用最多的应用应该就是支付宝和微信了吧,但是个人感觉从设计上看 支付宝的交互设计明显更胜一筹,要说到底是哪里强吧,我感觉就是支付宝的里面各种嵌套滑动给了应用更高的可玩性,不会显得那么单调.
于是,咱们就撸个支付宝首页玩吧! 哈哈哈

先摆上项目地址https://github.com/nokiafen/viewpro/tree/master/alihomepage

献上动图

ali_sc_shot.gif

是不是觉得我山寨了一个支付宝??? 冤枉!我只是偷了懒只实现了交互效果而已 其它区域都是截图啊 ,大兄弟!

怎么实现呢?其实只实现交互效果并不算太复杂,改bug倒是花了不少功夫 就一个类300来行就可以搞定了 不信看我截图

image.png

要达到这个效果最简单的就是自定义CoordinatorLayout.Behavior了 大概谈谈它的工作原理吧,CoordinatorLayout就是一个加强版的FrameLayout,它通过Behavior来与它的直接子控件进行通信,CoordinatorLayout会通过behavior来控制子控件的位置,以及统一分配滑动事件.

1.首先咋们定义一个behavior 给咱们布局里面最下面 NestSrollerView用(里面有一大堆可以滑动的东西)并且在布局文件里面NestScrollerView标签里面声明一下


image.png

这里的scroll_behavior 就是你自定义behavior的全包名

  1. 然后其它逻辑都在behavior了
    2.1 构造函数得重写
 public ScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
        scrollerRefresh = new Scroller(context);
        handler = new Handler();
    }

2.2 指定一个依赖,其实就是你想监听那个view的位置变化你就指定谁

 @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child,
                                   @NonNull View dependency) {
        if (dependency.getId() == R.id.function_area) {
            dependy = new WeakReference<View>(dependency);
            return true;
        }
        return super.layoutDependsOn(parent, child, dependency);
    }

2.3 处理依赖位置变化 ,如果你想要依据依赖位置的变化做一些逻辑操作

 @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent,
                                          @NonNull NestedScrollView child, @NonNull View dependency) {
        child.setTranslationY(dependency.getTranslationY()*2);

        float perchent = dependency.getTranslationY() / maxFunctionCollaped * 0.3f;
//        parent.findViewById(R.id.title_bar).setAlpha(1-perchent);
        if (dependency.getTranslationY() > -maxFunctionCollaped * 0.3f) {
            parent.findViewById(R.id.title_bar).setBackgroundResource(R.mipmap.top_search_back);
        } else {
            parent.findViewById(R.id.title_bar).setBackgroundResource(R.mipmap.search_bar_collapsing);
        }
        return super.onDependentViewChanged(parent, child, dependency);
    }

2.4 指定界面初始化时的布局位置,因为CoordinateLayout相当于帧布局 ,你要在这里指定一些位置关系(通常是view的上下关系,帧布局里面写比较费力,在代码里面直接码比较直接).

  @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child,
                                 int layoutDirection) {
        View function_area = parent.findViewById(R.id.function_area);
        View tigle_bar = parent.findViewById(R.id.title_bar);
        View  bottomLayout = parent.findViewById(R.id.bottom_layout);
        child.layout(0, function_area.getBottom(), parent.getMeasuredWidth(),
                function_area.getMeasuredHeight() + parent.getMeasuredHeight()+bottomLayout.getMeasuredHeight());
        maxFunctionCollaped = (int) (function_area.getMeasuredHeight()*0.5f);
        anim_root = child.findViewById(R.id.anim_root);
        scroll_content = child.findViewById(R.id.scroll_content);
        return true;
    }

2.5 嵌套滑动第一步 ,判断是否进行嵌套滑动 ,每次嵌套滑动就是从这个方法开始的


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

2.6 嵌套滑动第二步 如果第一步返回true 就会到这里来了

 @Override
    public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
                                       @NonNull NestedScrollView child, @NonNull View directTargetChild, @NonNull View target,
                                       int axes, int type) {
        scroller.abortAnimation();
        scrollerRefresh.forceFinished(true);
        isNestScrolling=false;
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
    }

2.7 嵌套滑动第三步 开始分配滑动距离

  @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                  @NonNull NestedScrollView child, @NonNull View target, int dx, int dy,
                                  @NonNull int[] consumed, int type) {
        View dependView = dependy.get();
        if (dy > 0 && dependView.getTranslationY() <= 0 && (anim_root.getTranslationY() == 0||anim_root.getTranslationY()==anim_root.getMeasuredHeight())) {   //刷新布局未偏移 , 向上滑动 且 上部分折叠区未折叠或正在折叠
            dy=(int)(Math.abs(dy));
            int currentTranslation = (int) dependView.getTranslationY();
            int targetDy = currentTranslation - dy;
            int concorrect = targetDy < -maxFunctionCollaped ? -maxFunctionCollaped : targetDy;
            dependView.setTranslationY(concorrect);
            consumed[1] = currentTranslation - concorrect;
        } else if (dy < 0 && dependView.getTranslationY() >= 0) {// 向下滑动 且上部分折叠区未折叠
            int currentTranslationY = (int) anim_root.getTranslationY();
            dy=-(int)(Math.abs(dy)*0.6f);
            float calculate = currentTranslationY - dy > anim_root.getMeasuredHeight() ? anim_root.getMeasuredHeight() : currentTranslationY - dy ;
           //calculate+=(calculate/anim_root.getMeasuredHeight())*dy; //阻尼效果
            anim_root.setTranslationY(calculate);
            scroll_content.setTranslationY(calculate);
            consumed[1] = (int) (calculate - currentTranslationY);
        } else if (dy > 0 && anim_root.getTranslationY() > 0) {  //向上滑动 ,刷新布局已经划出来
            int currentTranslationY = (int) anim_root.getTranslationY();
            int calculate = currentTranslationY - dy >= 0 ? currentTranslationY - dy : 0;
            anim_root.setTranslationY(calculate);
            scroll_content.setTranslationY(calculate);
            consumed[1] = dy;
        }

        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }

这里dy表示y方向 感应到的滑动量 dy>0表示向上滑动 dy<0表示向下滑动,然后你可以根据dy以及当前控件的位置来决定用哪一个控件来消耗这个dy ,然后把你的消耗值传给consumed[1] ,如果你把dy消耗完了,那里面嵌套的可滑动控件NestScrollerview就不会滑动内部内容了,如果没用完,那么NestScrollerview会紧接着onNestedPreScroll你自己定义的滑动继续滑动NestScrollerview里面的内容

2.8 嵌套滑动第四步:可滑动控件NestScrollerview 滑动完了(前提是它有得滑)紧接着就会到 onNestedScroll方法

 @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                               @NonNull NestedScrollView child, @NonNull View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed, int type) {
        if (dyUnconsumed < 0 && dependy.get().getTranslationY() < 0) {
            View dependView = dependy.get();
            int currentTranslation = (int) dependView.getTranslationY();
            int targetDy = currentTranslation - dyUnconsumed;
            int concorrect = targetDy > 0 ? 0 : targetDy;
            dependView.setTranslationY(concorrect);
            dyConsumed = currentTranslation - concorrect;
            isNestScrolling=true;
        }
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
                dyUnconsumed, type);
    }

如果可滑动控件NestScrollerview滑动(它是在滑动它里面的内容)完了 还有未消耗完的滑动量 你可以在这里拿到滑动量dyUnconsumed,继续定义紧接着NestScrollerview滑动完的其它滑动事件

2.9 惯性滑动处理 ---滑动结束的一种情况(不一定会调用)

    public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                                    @NonNull NestedScrollView child, @NonNull View target, float velocityX, float velocityY) {
        onRefreshEnd();
        Log.d("onNestedStroll","fling");
        return onDragEnd(velocityY);
    }

一般就是快速滑动马上抬手就会出现,这里会返回滑动速度给你,参考这个数值来处理滑动view的最终停留状态。

2.10 滑动结束的回调,不同于上个方法这个 一定会触发的

 @Override
    public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                   @NonNull NestedScrollView child, @NonNull View target, int type) {
        Log.d("onNestedStroll","onStopNestedScroll");
        onRefreshEnd();
        onDragEnd(600);
        isNestScrolling=false;
    }

滑动结束后 必须确定View的最终位置(到底是展开还是缩起来),摆在中间不太好看吧,这里需要借助scroll开展平滑动画来将你的view从中间态移动到你最终希望它到达的位置,达到一种弹性效果。

onNestedPreFling onStopNestedScroll两个方法处理滑动结束的逻辑比较统一也相对简单,而且逻辑基本上都是一样的。麻烦点是动画播放时进行嵌套移动以及如何分配滑动量给子view

博文里只能简述基本流程,详情请移步仓库
https://github.com/nokiafen/viewpro/tree/master/alihomepage

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容