Android M Preview发布后,我们获得了一个新的support library —— Android Design Support Library 用来实现Google的Material Design 提供了一系列符合设计标准的控件。

其中有众多的控件,其中最复杂,功能最强大的就是CoordinatorLayout,顾名思义,它是用来组织它的子views之间协作的一个父view。CoordinatorLayout默认情况下可理解是一个FrameLayout,它的布局方式默认是一层一层叠上去。
那么,CoordinatorLayout的神奇之处就在于Behavior对象了。
看下CoordinatorLayout.Behavior对象的 Overview

Interaction behavior plugin for child views of CoordinatorLayout.

A Behavior implements one or more interactions that a user can take on a child view. These interactions may include drags, swipes, flings, or any other gestures.

可知Behavior对象是用来给CoordinatorLayout的子view们进行交互用的。
Behavior接口拥有很多个方法,我们拿AppBarLayout为例。AppBarLayout中有两个Behavior,一个是拿来给它自己用的,另一个是拿来给它的兄弟结点用的,我们重点关注下AppBarLayout.ScrollingViewBehavior这个类。

我们看下这个类中的以下方法

0. dependency

 public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency)     {
    return dependency instanceof AppBarLayout;
}

这个方法告诉CoordinatorLayout,这个view是依赖AppBarLayout的,后续父亲可以利用这个方法,查找到这个child所有依赖的兄弟结点。

1. measure

public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)

这个是CoordinatorLayout在进行measure的过程中,利用Behavior对象对子view进行大小测量的一个方法。
在这个方法内,我们可以通过parent.getDependencies(child);这个方法,获取到这个child依赖的view,然后通过获取这个child依赖的view的大小来决定自身的大小。

2. layout

 public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection)

这个方法是用来子view用来布局自身使用,如果依赖其他view,那么系统会首先调用

public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) 

这个方法,可以在这个回调中记录dependency的一些位置信息,在onLayoutChild中利用保存下来的信息进行计算,然后得到自身的具体位置。

3. nested scroll

 public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes)
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) 
public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) 
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target) 

这几个方法是不是特别熟悉?我在Android嵌套滑动机制(NestedScrolling) 介绍过,这几个方法刚好是NestedScrollingParent的方法,也就是对CoodinatorLayout进行的一个代理(Proxy),即CoordinatorLayout自己不对这些消息进行处理,而是传递给子view的Behavior,进行处理。利用这样的方法,实现了view和view之间的交互和视觉的协同(布局、滑动)。

总结

可以看到CoodinatorLayout给我们实现了一个可以被子view代理实现方法的一个布局。这和传统的ViewGroup不同,子view从此知道了彼此之间的存在,一个子view的变化可以通知到另一个子view。CoordinatorLayout所做的事情就是当成一个通信的桥梁,连接不同的view。使用Behavior对象进行通信。

我们具体的实现可以参照 Android官方文档告诉我们的每一个方法的作用 进行重写,实现自己想要的各种复杂的功能。
https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.Behavior.html
有了这么一套机制,想实现组件之间的交互,就更加方便快捷啦~

Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性

NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView

图片描述

效果如上图【别嫌弃我】

在这之前,我们知道AndroidTouch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEventonInterceptTouchEventonTouchEvent

这种分发机制有一个漏洞:

如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。

也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下

NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper

以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

对,简单的话你就这么实现就好了。

这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法

    /**
     * Start a new nested scroll for this view.
     *
     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
     * method/{@link NestedScrollingChild} interface method with the same signature to implement
     * the standard policy.</p>
     *
     * @param axes Supported nested scroll axes.
     *             See {@link NestedScrollingChild#startNestedScroll(int)}.
     * @return true if a cooperating parent view was found and nested scrolling started successfully
     */
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。

那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:

一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent

二、dispatchNestedPreScroll

在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。

三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。

四、stopNestedScroll

结束整个流程。

整个对应流程是这样

子view父view
startNestedScrollonStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScrollonNestedPreScroll
dispatchNestedScrollonNestedScroll
stopNestedScrollonStopNestedScroll

一般是子view发起调用,父view接受回调。

我们最需要关注的是dispatchNestedPreScroll中的consumed参数。

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;

它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。


对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。

注:本文适合有一定java基础的童鞋看,至少明白注解Annotation是什么

贴上我的Android网络通信库地址
https://github.com/MyLifeForTheOrc/gm-httpengine-studio

最近在annotation分支上工作,就为了增加注解支持。
目标是像ButterKnife一样酷炫,现在也差不多。

首先看下改进后的(酷炫)使用方法,如果我需要做一个http请求,只需要以下几步:

定义API

package org.gemini.httpengine.examples;

import org.gemini.httpengine.annotation.GET;
import org.gemini.httpengine.annotation.Path;
import org.gemini.httpengine.annotation.TaskId;
import org.gemini.httpengine.library.OnResponseListener;

/**
 * Created by geminiwen on 15/5/21.
 */
public interface UserAPI {
    interface TASKID {
        String TASK_GET_LOGIN = "login";
    }

    @Path("http://www.baidu.com")     //定义URL地址
    @TaskId(TASKID.TASK_GET_LOGIN)    //给这个请求加一个taskId,标识请求
    @GET                              //标明这个请求是一个GET请求
    void login(OnResponseListener l,
               String username,
               String password);
}

在Activity中调用API

    @Override
    public void onClick(View v) {
        if(v == mTestButton) {
            UserAPI api = InjectFactory.inject(UserAPI.class); //注入API实例
            api.login(this, "geminiwen", "password");   //调用接口
        }
    }

获取接口

首先实现OnResponseListener接口

    @Override
    public void onResponse(GMHttpResponse response, GMHttpRequest request) {
        byte[] result = null;
        try {
            result = response.getRawData();        //获取数据
        } catch (Exception e) {
            Log.e("error", "wtf?", e);
        }

//        Toast.makeText(this,result,Toast.LENGTH_LONG).show();
    }

使用了注解的方式,是不是感觉很干净彻底?
但是这里只有接口,并没有实现类啊,这到底是怎么做到的呢?

OK,我们看看它背后做了什么。

这里我们使用Android Studio举例

首先可以看下Android Application Module下面的build文件夹

clipboard.png
其他文件都很正常,除了两个"不速之客",UserAPI$$APIINJECTOR.javaUserAPI$$APIINJECTOR.class。没错,这就是使用Annotation Processor生成的java文件了。
我们看看生成了什么。

clipboard.png

把它格式化一下如下:

package org.gemini.httpengine.examples;

import org.gemini.httpengine.library.*;

public class UserAPI$$APIINJECTOR implements org.gemini.httpengine.examples.UserAPI {
    
    public void login(org.gemini.httpengine.library.OnResponseListener l, java.lang.String username, java.lang.String password) {
        final String FIELD_USERNAME = "username";
        final String FIELD_PASSWORD = "password";
        GMHttpParameters httpParameter = new GMHttpParameters();
        httpParameter.setParameter(FIELD_USERNAME, username);
        httpParameter.setParameter(FIELD_PASSWORD, password);
        GMHttpRequest.Builder builder = new GMHttpRequest.Builder();
        builder.setHttpParameters(httpParameter);
        builder.setTaskId("login");
        builder.setUrl("http://www.baidu.com");
        builder.setMethod("GET");
        builder.setOnResponseListener(l);
        GMHttpService service = GMHttpService.getInstance();
        service.executeHttpMethod(builder.build());

    }
}

这里的代码就是调用GMHttpEngine里面带的API,进行HTTP的请求。

OK,我们得到结论了,它的秘密就是库利用一些特殊的特性,帮你生成了一个实现类,并利用InjectFactory.inject这个方法,把这个实现类用反射的方式,生成出来,返回给你的接口。

那新问题产生了,生成代码这么叼的事是怎么做到的呢。我们必须了解apt这个东西的存在
给个APT介绍的传送门(鸟语):http://docs.oracle.com/javase/7/docs/technotes/guides/apt/

这时候我们知道了有Annotation Processor这个东西的存在,我们的目标就是自定义一个Annotation Processor了。

看第一个接口叫AbstractProcessor,完全限定名是:javax.annotation.processing.AbstractProcessor;
它是一个抽象类,所以我们要自定义一个类,去继承它,它最主要的就是里面的process接口

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<TypeElement, APIClassInjector> targetClassMap = findAndParseTargets(roundEnv);

        for (Map.Entry<TypeElement, APIClassInjector> entry : targetClassMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            APIClassInjector injector = entry.getValue();
            try {
                String value = injector.brewJava();

                JavaFileObject jfo = filer.createSourceFile(injector.getFqcn(), typeElement);
                Writer writer = jfo.openWriter();
                writer.write(value);
                writer.flush();
                writer.close();
            } catch (Exception e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), typeElement);
            }
        }


        return false;
    }

看见里面一个叫JavaFileObject的对象没有,它就是生成java文件最重要的对象了。这里我们干的事情,就是分析我们的注解(Annotation)然后,生成相应的代码,利用JavaFileObject进行写入,就好了。

当然,你需要告诉Annotation Processor你要处理哪一些注解,具体方法就是重写它的getSupportedAnnotationTypes方法

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(Path.class.getCanonicalName());
        return supportTypes;
    }

因为Path注解是GMHttpEngine所使用的核心注解,所以我这里就写了这个,从接口看,它可以支持一批的注解,对于不同的注解要有不同的处理方式。

除此之外,你需要定义resources文件夹,新建一个文件叫javax.annotation.processing.Processor
里面的内容就是你的Annotation Processor的完全限定名。目录结构如下:

clipboard.png

里面的内容是

clipboard.png

最后说了这么多,再整理下神奇的流程。

  1. 首先我们要定义一下自己的注解。
  2. 自定义我们自己的Annotation Processor,即继承AbstractProcessor类。
  3. process方法中分析注解,生成java代码。
  4. resources\META-INF\services\javax.annotation.processing.Processor文件中注册类名。
  5. 导入就可以使用注解标识API请求自动生成代码啦~

噢~这里要说下在开发注解功能碰到的坑:

如果使用AndroidStudio 需要注意的是,Android Library并不是普通的JavaSE,所以并没有提供javax的一些功能,所以,在新建Module的时候不能选Android Library而应该选Java Library,而且因为它只在编译的时候使用到JavaSE的功能,所以并不用担心在手机上跑的时候会出问题。

好了,想了解更多,欢迎star 欢迎fork

https://github.com/MyLifeForTheOrc/gm-httpengine-studio

欢迎在SegmentFault上一起讨论问题~

也欢迎给我邮件 geminiwen@segmentfault.com

Android中 View的绘制分为三步。

  1. measure —— 用于得知(子)View的大小
  2. layout —— 摆放好(子)View的位置
  3. draw —— 真正绘制View的内容

因为Android的layout系统是一个考虑好相对布局的一个系统,我们知道ViewGroup是继承于View的,思想上可以把ViewGroup当成是一个View的组合

我们看看在三个函数里分别做了什么。

onMeasure

这个函数主要传入两个参数

void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

一个代表宽度的参数,一个代表高度的参数。
这里的宽度参数是父容器的一些参数,它并不仅仅是数值,它用了位运算,根据相应的掩码能得到父容器能给与子容器的宽度,有EXACTLY,UNSPECIFIEDAT_MOST三个值,分别说明:

  1. EXACTLY 父容器希望子视图有它指定的大小
  2. UNSPECIFIED 父容器可以无限容纳子容器,子视图要多大都可以
  3. AT_MOST 父容器指定了最大的大小,让子视图自行决定大小。

根据这些算出大小后,父容器就知道自己应该占多少的空间,同时报告给它的父容器,在这个时候,也可以把子容器应该有的大小记下来,一会在onLayout中用。

onLayout

这个函数声明如下:

void onLayout(boolean changed, int l, int t, int r, int b)

第一个参数表明大小位置是否变动过,剩下的4个参数分别代表该容器的lefttoprightbottom,容器可以根据这个参数,直接得出它现在的宽度,高度,位置等,如果它是一个ViewGroup,那么它可以根据这些参数为它的子视图进行布局。

onDraw

好了,这个是最后的一个步骤了,就是画。
传入的参数就是Canvas 一个画布,你可以在这个画布上绘制你要的各种样式,
这时候调用getWidthgetHeight都是安全的,因为已经经过了onLayout的步骤了。

以上是Android中自定义View最重要的三个步骤,理解了这三步,就在准确的位置,准确的大小画出你想要的图形了。