发现这东西其实也没那么复杂,况且在相互嵌套

时间:2019-09-30 15:37来源:美高梅手机游戏网站
标签不用说,基本所有的app都会用到这种需求,不管是电商app商品的筛选标签,还是新闻app的搜索历史记录标签,体育app的日期和赛事赛选标签,团购app的省市区的多级标签,都会有它们

标签不用说,基本所有的app都会用到这种需求,不管是电商app商品的筛选标签,还是新闻app的搜索历史记录标签,体育app的日期和赛事赛选标签,团购app的省市区的多级标签,都会有它们的身影.这次,为大家介绍自己的一个标签控件-FlowView,看看它的展现形式是不是和日常开发中的很多地方都有碰撞。

由于之前做一个小项目的时候其中需要展示一个页面,如图所示:

CoordinatorLayout

终于到这个控件了,其实我的内心是忐忑的,因为我其实一直想要深入的理解 CoordinatorLayout+Behavior的原理,但是又苦于太难懂了,以前也零零碎碎研究过几次,最后都以失败告终。这次是没办法,MaterialDesign 篇到这里也快结束了,做事还是要有始有终,于是这两天好好研究了一下,发现这东西其实也没那么复杂。

CoordinatorLayout直接继承了ViewGroup,说明最少重写了 onMeasure和 onLayout 方法,说不定还有 onMeasureChild和 onLayoutChild 方法。然后我们在看,CoordinatorLayout 是用来处理嵌套滑动的,那么onTouchEvent()和 onInterceptTouchEvent()方法肯定也跑不掉。

好了,按照惯例,我们先从构造方法以及 attributes 属性开始看吧。

1.展示类型:

美高梅手机游戏网站 1

attributes

<declare-styleable name="CoordinatorLayout">
    <attr format="reference" name="keylines"/>
    <attr format="reference" name="statusBarBackground"/>
</declare-styleable>

<declare-styleable name="CoordinatorLayout_Layout">
    <attr name="android:layout_gravity"/>
    <attr format="string" name="layout_behavior"/>
    <attr format="reference" name="layout_anchor"/>
    <attr format="integer" name="layout_keyline"/>
    <attr name="layout_anchorGravity">
        <flag name="top" value="0x30"/>            
        <flag name="bottom" value="0x50"/>            
        <flag name="left" value="0x03"/>            
        <flag name="right" value="0x05"/>            
        <flag name="center_vertical" value="0x10"/>            
        <flag name="fill_vertical" value="0x70"/>           
        <flag name="center_horizontal" value="0x01"/>            
        <flag name="fill_horizontal" value="0x07"/>            
        <flag name="center" value="0x11"/>          
        <flag name="fill" value="0x77"/>            
        <flag name="clip_vertical" value="0x80"/>            
        <flag name="clip_horizontal" value="0x08"/>            
        <flag name="start" value="0x00800003"/>            
        <flag name="end" value="0x00800005"/>
    </attr>
    <attr format="enum" name="layout_insetEdge">          
        <enum name="none" value="0x0"/>            
        <enum name="top" value="0x30"/>            
        <enum name="bottom" value="0x50"/>            
        <enum name="left" value="0x03"/>            
        <enum name="right" value="0x03"/>            
        <enum name="start" value="0x00800003"/>            
        <enum name="end" value="0x00800005"/>
    </attr>
    <attr name="layout_dodgeInsetEdges">            
        <flag name="none" value="0x0"/>            
        <flag name="top" value="0x30"/>            
        <flag name="bottom" value="0x50"/>            
        <flag name="left" value="0x03"/>            
        <flag name="right" value="0x03"/>            
        <flag name="start" value="0x00800003"/>            
        <flag name="end" value="0x00800005"/>            
        <flag name="all" value="0x77"/>
    </attr></declare-styleable>

可以直接设置在 CoordinatorLayout 节点上的属性有两个

  • keylines 一个比较奇怪的属性,好像是一个布局解决方案吧。比较鸡肋,没有人用过它
  • statusBarBackground 状态栏背景颜色

剩下的都是只能作用在 CoordinatorLayout 的直接子节点上的属性

  • layout_behavior 这个属性大家都很熟悉,因为Behavior 是嵌套滑动的精华。辅助Coordinator对View进行layout、nestedScroll的处理
  • layout_anchor 将其固定在某个 view 上面,可以理解成依附
  • layout_keyline 同上
  • layout_anchorGravity 这个容易理解,依附在控件上的位置
  • layout_insetEdge 用于避免布局之间互相遮盖
  • layout_dodgeInsetEdges 用于避免布局之间互相遮盖

布局这一块没什么说的,CoordinatorLayout作为顶级节点,然后根据实际需求使用对应的控件和属性就行了,这里我就不做过多的赘述。

标题 标签 可选/不可选

GIF.gif

源码

首先,刚刚我们猜测了肯定会重写 onMeasure 方法,那么我们就从 onMeasure 方法开始看。
onMeasure 方法里面有这么一段方法,我 copy 出来给大家看一下

for (int i = 0; i < childCount; i++) {
    final View child = mDependencySortedChildren.get(i);
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    int keylineWidthUsed = 0;
    if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) {
        final int keylinePos = getKeyline(lp.keyline);
        final int keylineGravity = GravityCompat.getAbsoluteGravity(
                    resolveKeylineGravity(lp.gravity), layoutDirection)
                    & Gravity.HORIZONTAL_GRAVITY_MASK;
        if ((keylineGravity == Gravity.LEFT && !isRtl)
                    || (keylineGravity == Gravity.RIGHT && isRtl)) {
                keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos);
        } else if ((keylineGravity == Gravity.RIGHT && !isRtl)
                    || (keylineGravity == Gravity.LEFT && isRtl)) {
                keylineWidthUsed = Math.max(0, keylinePos - paddingLeft);
        }
    }

    int childWidthMeasureSpec = widthMeasureSpec;
    int childHeightMeasureSpec = heightMeasureSpec;
    if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) {
        // We're set to handle insets but this child isn't, so we will measure the
        // child as if there are no insets
        final int horizInsets = mLastInsets.getSystemWindowInsetLeft()
                    + mLastInsets.getSystemWindowInsetRight();
        final int vertInsets = mLastInsets.getSystemWindowInsetTop()
                    + mLastInsets.getSystemWindowInsetBottom();

        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    widthSize - horizInsets, widthMode);
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    heightSize - vertInsets, heightMode);
    }

    final Behavior b = lp.getBehavior();
    if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
    }

    widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                lp.leftMargin + lp.rightMargin);

    heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin);
    childState = ViewCompat.combineMeasuredStates(childState,
                ViewCompat.getMeasuredState(child));
}

这是一段遍历子 View 的操作,首先判断 keyLine,这个属性我们不关心,直接跳过,然后就是获取子 view 的 Behavior,然后判断是否为空,在根据 Behavior 去 measure 子 view。这里我们能看到子 view 的 Behavior 是保存在 LayoutParams里面的,所以这个 LayoutParams 肯定是重写的。然后我们 Behavior 一般是直接写到 xml 布局的子节点上对吧,所以可以判断子 view 的 Behavior 是在View 解析 xml 的时候,读取到 Behavior 节点,然后赋值给 LayoutParams。LayoutInflate 的源码我就不带着大家去读了,我贴出关键代码

ViewGroup.LayoutParams params = null;
try {
    params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
    // Ignore, just fail over to child attrs.
}

这里的group 就是 parent强转的,子 View 的 LayoutParams 是通过父 view 的generateLayoutParams()创建,于是我们去看 CoordinatorLayout 的generateLayoutParams方法。

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

额,尴尬了,直接去看构造方法把

LayoutParams(Context context, AttributeSet attrs) {
    super(context, attrs);

    final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CoordinatorLayout_Layout);

    this.gravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
                Gravity.NO_GRAVITY);
    mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                View.NO_ID);
    this.anchorGravity = a.getInteger(
                R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                Gravity.NO_GRAVITY);

     this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
                -1);

    insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
        dodgeInsetEdges = a.getInt(
                R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
    mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_Layout_layout_behavior);
    if (mBehaviorResolved) {
        mBehavior = parseBehavior(context, attrs, a.getString(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior));
    }
    a.recycle();

    if (mBehavior != null) {
        // If we have a Behavior, dispatch that it has been attached
            mBehavior.onAttachedToLayoutParams(this);
    }
}

好,这里我们可以看到,我们之前设置的一些layout_anchor、anchorGravity、layout_keyline、layout_behavior等属性,我就不过多赘述了,今天的重点是 Behavior 呢,我们看parseBehavior()方法,这个方法创建了一个 Behavior

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }

    final String fullName;
    if (name.startsWith(".")) {
        // Relative to the app package. Prepend the app package name.
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        // Fully qualified package name.
        fullName = name;
    } else {
        // Assume stock behavior in this package (if we have one)
        fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                ? (WIDGET_PACKAGE_NAME + '.' + name)
                : name;
    }

    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

可以看到,内部是通过反射的方式创建的Behavior,然后调用的两个参数的构造函数,所以如果想要试用behavior就必须实现它的构造函数,不然就会报异常。哈哈,反正我第一次创建 Behavior 的时候运行时报错了,说找不到两个参数的构造方法。

好,接下来开始到重点了。measure 方法结束了之后,应该开始布局了,所以我们解析来去看 onLayout()方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

        if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
            onLayoutChild(child, layoutDirection);
        }
    }
}

遍历子 view,如果 behavior.onLayoutChild()方法返回true,则不会调用 CoordinatorLayout 的 onLayouChild()方法,由此可得出结论,重写 Behavior 的 onLayoutChild 方法是用来自定义当前 View 的布局方式

此时,布局结束,我们的 CoordinatorLayout 静态页面已经完成,接下来,我们要看的是滑动的时候,CoordinatorLayout 怎么处理。
我们来简单回顾一下 ViewGroup 的事件分发机制,首先 disPatchTouchEvent()被调用,然后调用 onInterceptTouchEvent 判断是否允许事件往下传,如果允许则丢给子 View的disPatchTouchEvent 来处理,如果不允许或者允许后子 view没有消费掉事件,则 先后调用自己的 onTouchListener 和 OnTouchEvent来消费事件。
然后我们来根据这个顺序看 CoordinatorLayout 的事件处理顺序,首先看 disPatchTouchEvent 方法, 这个方法,没有重写,那么略过直接看 onInterceptTouchEvent 方法。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    // Make sure we reset in case we had missed a previous important event.
    if (action == MotionEvent.ACTION_DOWN) {
    //重置状态
        resetTouchBehaviors();
    }

    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
    //重置状态
        resetTouchBehaviors();
    }

    return intercepted;
}

没什么好说的,继续追performIntercept()方法

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;

    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    final List<View> topmostChildList = mTempList1;
    getTopSortedChildren(topmostChildList);

    // Let topmost child views inspect first
    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();

        if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
            // Cancel all behaviors beneath the one that intercepted.
            // If the event is "down" then we don't have anything to cancel yet.
            if (b != null) {
                if (cancelEvent == null) {
                    final long now = SystemClock.uptimeMillis();
                    cancelEvent = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                }
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        b.onInterceptTouchEvent(this, child, cancelEvent);
                        break;
                    case TYPE_ON_TOUCH:
                        b.onTouchEvent(this, child, cancelEvent);
                        break;
                }
            }
            continue;
        }

        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                mBehaviorTouchView = child;
            }
        }

        // Don't keep going if we're not allowing interaction below this.
        // Setting newBlock will make sure we cancel the rest of the behaviors.
        final boolean wasBlocking = lp.didBlockInteraction();
        final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
        newBlock = isBlocking && !wasBlocking;
        if (isBlocking && !newBlock) {
            // Stop here since we don't have anything more to cancel - we already did
            // when the behavior first started blocking things below this point.
            break;
        }
    }

    topmostChildList.clear();

    return intercepted;
}

遍历所有子 View,调用了符合条件的 view 的 Behavior.onInterceptTouchEvent/onTouchEvent方法
然后我们来看 onTouchEvent 方法

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // Safe since performIntercept guarantees that
        // mBehaviorTouchView != null if it returns true
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        handled |= super.onTouchEvent(ev);
    } else if (cancelSuper) {
        if (cancelEvent == null) {
            final long now = SystemClock.uptimeMillis();
            cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
        }
        super.onTouchEvent(cancelEvent);
    }

    if (!handled && action == MotionEvent.ACTION_DOWN) {

    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return handled;
}

重点在if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))这句话上,如果先前有子 view 的Behavior 的 onInterceptTouchEvent 返回了 true,则直接调用这个子 view 的 Behavior 的 onTouchEvent。否则就继续走一遍performIntercept(ev, TYPE_ON_TOUCH),即:执行所有含有 Behavior 的子 view 的 Behavior.onTouchEvent方法。

咳咳~~好了, 上面两个方法的各种逻辑判断有点绕,我也是被绕了很久,没看懂没事,直接看杰伦
我们再来回过头看这两个方法,其最终都是调用了 Behavior 的 onInterceptTouchEvent 和 onTouchEvent 方法,然后各种条件判断就是什么时候调用这两个方法。

  • onInterceptTouchEvent
    1.在 CoordinatorLayout 的 onInterceptTouchEvent 方法中杯调用 。
    2.调用顺序:按照 CoordinatorLayout 中 child 的添加倒叙进行调用
    3.运行原理:
    如果此方法在 down 事件返回 true,那么它后面的 view 的 Behavior 都执行不到此方法;并且执行 onTouchEvent 事件的时候只会执行此 view 的 Behavior 的 onTouchEvent 方法。
    如果不是 down 事件返回 true,那么它后面的 view 的 Behavior 的 onInterceptTouchEvent 方法都会执行,但还是只执行第一个 view 的 Behavior 的 onTouchEvent 方法
    如果所有的 view 的 Behavior 的onInterceptTouchEvent 方法都没有返回 true,那么在 CoordinatorLayout 的 onTouchEvent 方法内会回调所有 child 的 Behavior 的 onTouchEvent 方法
    4.CoordinatorLayout 的 onInterceptTouchEvent 默认返回 false,返回值由child 的 Behavior 的 onInterceptTouchEvent 方法决定

  • onTouchEvent
    1.在 CoordinatorLayout 的 onTouchEvent 方法中被调用
    2.调用顺序:同上
    3.在上面 onInterceptTouchEvent 提到的所有 Behavior 的 onTouchEvent 都返回 false 的情况下,会遍历所有 child 的此方法,但是只要有一个 Behavior 的此方法返回 true,那么后面的所有 child 的此方法都不会执行
    4.CoordinatorLayout 的 onTouchEvent默认返回super.onTouchEvent(),如果有 child 的 Behavior 的此方法返回 true,则返回 true。

然后再来说一下嵌套滑动把,我们都知道 CoordinatorLayout 的内嵌套滑动只能用 NestedScrollView 和 RecyclerView,至于为什么呢。我相信很多人肯定点开过 NestedScrollView 和 RecyclerView 的源码,细心的同学肯定会发现这两个类都实现了NestedScrollingChild接口,而我们的 CoordinatorLayout 则实现了NestedScrollingParent的接口。这两个接口不是这篇文章的重点,我简单说一下,CoordinatorLayout 的内嵌滑动事件都是被它的子NestedScrollingChild实现类处理的。而子View 在滑动的时候,会调用NestedScrollingParent的方法,于是 CoordinatorLayout 再NestedScrollingParent的实现方法中,调用了 Behavior 的对应方法。

单选/多选 操作按钮

遇到这样的需求的时候,我的第一印象就是想用 RecyclerView 的嵌套使用。但是由于 RecyclerView 相互嵌套用在这种情况下比较浪费,况且在相互嵌套的情况下出了一些问题我暂时不知道怎么解决。

总结

好了,分析到这里,其实我感觉,我们更应该去了解一下NestedScrollingParent和NestedScrollingChild的嵌套滚动机制。简单点说,就是 child(RecycleView) 在滚动的时候调用了 parent(CoordinatorLayout) 的 对应方法,而我们的 Behavior,则是在 parent 的回调方法中,处理了其他child 的伴随变化。
本质上,我们可以通过自定义控件的方式实现,但是 Google帮我们封装的这一套控件很解耦啊、很牛逼啊、很方便啊,所以我用它。


自动/手动列数 指定行数/指定宽度

所以我用了另外一个方式进行替代:GirdLayout 动态添加 view 。
说到 GirdLayout (网格布局)它是 Android 4.0 以后引入的一个新布局。
其中最重要的两个属性是:

Behavior

直接看官网描述吧

  • Interaction behavior plugin for child views of CoordinatorLayout.
    对 CoordinatorLayout 的 child 交互行为的插件。

  • 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.
    一个child 的Behavior 可以实现一个或者多个 child 的交互,这些交互可以包括拖动、滑动、惯性以及其他手势。

按照国际惯例,我们应该先看 Public methods。但是,As a Developer,我们是不需要持有 Behavior 引用的。所有的 Behavior CoordinatorLayout都已经帮我们管理好了,所以,我们可以先不用关心公共方法。

好了,那我们聊两点大家需要关心的。

水平/非水平滑动 网格线

android:layout_columnWeight 
android:layout_rowWeight

Dependent

Dependent: adj.依赖的

顾名思义,Dependent 就是依赖的意思。在 Behavior 中,就是使某个 View依赖一个指定的 view,使得被依赖的 view 的大小位置改变发生变化的时候,依赖的 view 也可以做出相应的动作。
常见的方法有两个

  • public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)

确定所提供的child 是否有另一个特点兄弟 View 的依赖
在一个CoordinatorLayout 布局里面,这个方法最少会被调用一次,如果对于一个给定的 child 和依赖返回 true,则父CoordinatorLayout 将:
1.在被依赖的view Layout 发生改变后,这个 child 也会重新 layout。
2.当依赖关系视图的布局或位置变化时,会调用 onDependentViewChange

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

响应依赖 view 变化的方法
无论是依赖 view的尺寸、大小或者位置发生改变,这个方法都会被调用,一个 Behavior 可以使用此方法来适当的更新响应child
view 的依赖关系由layoutDependsOn 或者child 设置了another属性来确定。
如果 Behavior 改变了 child 的大小或位置,它应该返回 true,默认返回 false。

好了,说了这么久,我们来动手写一个小 Demo 吧。
大家都知道 FloatActionBar 在 CoordinatorLayout 布局中,处于屏幕底部时,然后 SnackBar 弹出来之后,会自动把 FloatActionBar 顶上去。就像酱紫

美高梅手机游戏网站 2

FloatActionBar.gif

布局文件和代码都贼简单~如下

<android.support.design.widget.CoordinatorLayout
 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"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 tools:context=".TestActivity">

 <android.support.design.widget.FloatingActionButton
    android:id="@+id/bt"
    android:layout_gravity="bottom|right"
    app:layout_behavior=".behavior.FABBehavior"
    android:layout_width="wrap_content"
    android:text="FloatActionBar"
    android:layout_height="wrap_content"/>

</android.support.design.widget.CoordinatorLayout>

findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Snackbar.make(v,"自定义 FloatActionBar", 1500).show();
        }
    });

这里就不一步一步去纠结了,FloatingActionButton内部有一个默认的 Behavior,这个 Behavior 实现了很多效果,我们先跳过吧。
然后我们来自定义一个 FloatingActionButton,实现 SnackBar 弹出的时候顶上去。

实现思路:我们要在SnackBar 在弹出的时候跟着一起往上位移,也就是说我们要监听 SnackBar 的位移事件,那么我们可以layoutDependsOn判断目前发生变化的 view 是不是 SnackBar,如果是,则返回 true,onDependentViewChanged方法,在onDependentViewChanged里面改变 MyFloatingActionButton 的位置。

我要自己实现的效果:

美高梅手机游戏网站 3

MyFloatActionBar.gif

代码实现,xml 布局(这里我图方便,就用一个 Button 代替了,样式也没改。。。主要是因为懒)

<android.support.design.widget.CoordinatorLayout
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".TestActivity">

<Button
    android:id="@+id/bt"
    android:layout_gravity="bottom|right"
    app:layout_behavior=".behavior.FABBehavior"
    android:layout_width="wrap_content"
    android:text="FloatActionBar"
    android:layout_height="wrap_content"/>
</android.support.design.widget.CoordinatorLayout>

这里其实就是一个 CoordinatorLayout 里面包裹了一个 Button,关键代码就是“app:layout_behavior=".behavior.FABBehavior"”,然后我们需要去写这个 FABBehavior 就行了。逻辑很简单,代码如下:

public class FABBehavior extends CoordinatorLayout.Behavior {

public FABBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof Snackbar.SnackbarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
    child.setTranslationY(translationY);
    return true;
}
}

很简单的代码逻辑,我就不写注释了,这里记得写两个参数的构造方法就行了。

然后我们再来看一个基于 Dependent 的 Demo

美高梅手机游戏网站 4

BottomBehavior.gif

好了,直接贴代码吧,很简单的。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">


    <android.support.v7.widget.Toolbar
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_scrollFlags="scroll|enterAlways"
        app:navigationIcon="@mipmap/abc_ic_ab_back_mtrl_am_alpha"
        app:title="标题">

    </android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:tag="Tag"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>


<TextView
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:layout_gravity="bottom"
    android:background="@color/colorPrimary_pink"
    android:gravity="center"
    android:text="我是底部导航栏"
    android:textColor="@color/white"
    app:layout_behavior=".behavior.BottomBehavior"/>
</android.support.design.widget.CoordinatorLayout>

好了,很简单的布局,需要注意的是,这个的 RecycleView 也设置了一个 Behavior 哦,这个 Behavior 的使用我们在上一篇博客已经讲过了哦。然后给我们的“底部导航栏设置一个 Behavior,使其在 RecycleView 向上滚动的时候能够隐藏起来。直接贴代码吧

public class BottomBehavior extends CoordinatorLayout.Behavior {
public BottomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

//依赖 AppBarLayout 的写法
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int delta = dependency.getTop();
    child.setTranslationY(-delta);
    return true;
}

//-----------依赖 RecycleView 的写法-----------
//    @Override
//    public boolean layoutDependsOn(CoordinatorLayout parent, final View child, View dependency) {
//        boolean b = "Tag".equals(dependency.getTag());
//        if (b && this.child == null) {
//            this.child = child;
//            ((RecyclerView) dependency).addOnScrollListener(mOnScrollListener);
//        }
//        return b;
//    }
//
//    View child;
//    RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
//        @Override
//        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//            super.onScrolled(recyclerView, dx, dy);
//            float translationY = child.getTranslationY();
//            translationY += dy;
//            translationY = translationY > 0 ? (translationY > child.getHeight() ? child.getHeight() : translationY) : 0;
//            Log.e("translationY", translationY + "");
//            child.setTranslationY(translationY);
//        }
//    };

}

这里我用了两种实现方式,推荐使用第一种。因为第二种其实在 Activity 里面也是可以实现的。之所以讲第二种写法是因为,我们在layoutDependsOn里面是可以获取到 CoordinatorLayout 里面所有child 的引用,直接 CoordinatorLayout.findViewWithTag()即可,然后我们可以为所欲为,哈哈哈,看实际需求吧。

(因为展示的内容比较多,所以gif压的有点厉害 !!!

其中第一个表示的是列数,也就是总共要分为几列。第二个表示的是行数,也就是说要分成几行。显然,这很符合这个页面的展示要求,但是作为商品的部分展示,我们又不能知道商品的数量,所以我们不能在 xml 里面设置。所以我们需要动态添加 view 。let's do it.

Nested

Nested 机制要求 CoordinatorLayout 包含了一个实现了 NestedScrollingChild 接口的滚动控件,如 RecycleView、NestedScrollView、SwipeRefreshLayout等。然后我们 Behavior 里面的如下几个方法会被回调哦~

onStartNestedScroll(View child, View target, int nestedScrollAxes)
onNestedPreScroll(View target, int dx, int dy, int[] consumed)
onNestedPreFling(View target, float velocityX, float velocityY)
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
onStopNestedScroll(View target)

哦,对了,onStartNestedScroll 方法返回 ture表示要接受这个事件,后面的方法才会被调用哦。
看名字应该都能看得懂这些方法在哪里会被回调吧,好了,那我们来实现一个小 Demo 吧,看图~

美高梅手机游戏网站 5

NestedDemo.gif

还是上面这个 Demo,xml 布局里面添加一个 FAB,给 FAB 设置一个 Tag,然后CoordinatorLayout.findViewWithTag()找到 FAB 的引用,onStarNestedScroll 返回 true 表示要接受这个事件,onNestedPreScroll 里面根据滚动的参数 dy 判断是上滑还是下拉,然后调用 FAb 的 hide 和 show 方法即可。

public class BottomBehavior extends CoordinatorLayout.Behavior {
public BottomBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

FloatingActionButton mFab;

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    if (mFab == null)
        mFab = (FloatingActionButton) parent.findViewWithTag("FAB");
    return dependency instanceof AppBarLayout;
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int delta = dependency.getTop();
    child.setTranslationY(-delta);
    return true;
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
    return true;
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    if (dy>10){
        mFab.hide();
    }else if (dy<-10){
        mFab.show();
    }
}
}

好了,写完这几个 Demo,Behavior 的知识点算是基本上讲完了,可能有些同学还是一头雾水。Wtah?这就讲完了? 我还没学会定制那些牛逼的 Behavior动画。
好吧,其实我也是先学了简书、掘金上面好几个热门的 Behavior 应用,才开始研究源码的。说实话,在写这篇文章之前,我真的不会用 Behavior。

废话说得有点多,没讲重点。那我先说重点吧,看懂了 Behavior 的原理再去看 Behavior 的 Demo 简直不要太简单。其实就是一些基本功底和一些属性动画的集成,让就成了高大上的 Behavior 动画。

来吧,分析几个别人的 Behavior Demo。
分析之前先郑重声明:1、我没有获得作者的授权,如果觉得我侵权了,请联系我。2、如果有评论措辞不当的地方还请多多包涵,仅代表个人观点,不针对任何人。3、还没想好,以后想到再添加


美高梅手机游戏网站 62. gif1美高梅手机游戏网站 72.gif2美高梅手机游戏网站 83.gif3美高梅手机游戏网站 94 .gif4

整体思路:
recyclerView + GirdLayout 商品的所有相关信息通过 LayoutInflater 转换为 view ,GirdLayout 动态添加 view ,RecyclerView 实现多个不同类型的商品。

Demo1

美高梅手机游戏网站 10

Demo1.gif

原文地址:传送门

我的项目中也用到了这个效果,当时我是基于 RecycleView 做了二次封装实现的。蜜汁尴尬,看到前两天学习 Behavior 的时候果断重构了代码。

Demo 实现分析:就是一个 AppBarLayout 使用了ScrollFlags 属性实现了折叠效果,然后CollapsingToolbarLayout实现了图片的视差滚动。关于 AppBarLayout 的使用请看我上一篇文章。然后再加了一个下拉放大的效果,这个需要我们自己定制。
1.继承AppBarLayout.Behavior,在任意包含 CoordinatorLayout 的方法里面获取使用 CoordinatorLayout.findViewWithTag()方法获取到需要下拉放大的 ImageView,记得设置 ImageView 的ClipChildren属性为 false。(不知道这个属性功能的朋友自行找度娘或者 Google)
2.重新onNestedPreScroll,在这个方法里面去根据下拉距离Scale ImageView 即可。
3.手指松了之后还原,一个属性动画解决
4.没有4了,已经实现,具体代码去看别人的源码吧,真的很容易实现,只是不知道的人不会而已。


以上9种设置基本都可以相互组合,因此FlowView多种展现类型:

其中涉及两个部分:
1.在添加 view 的时候,View 之中涉及图片大小,由于我们设置列数,由于 Gridlayout 宽度设置 match_parent ,所以整个布局的宽度可以确定。但是如果设置行数的话,那么 Gridlayout 长度设置为 match_parent 的话,那么会对于里面的布局会比较难看,但是如果设置固定的长度,那么对于手机适配这一方面,也会有比较大的差别。所以最好的方法是根据图片的宽度来设置高度。设置方法有多种,其中参考网上觉得最容易的一种是重写 ImageView 方法里面的 onMeasure() 方法。将长度与宽度相互联系。

Demo2

美高梅手机游戏网站 11

Demo2.gif

哈哈,是不是很酷炫,反正当时我看完之后感觉这一定是一个很酷炫的动画。
不用,其实两个 Behavior 就可以搞定了。
先来分析一下有哪些效果吧。
1.下拉放大封面图片
2.上拉头像跟着个人信息栏目往上走
3.上拉头像缩小
4.个人信息栏和工具栏视差滚动
5.RecycleView 等其他栏目折叠完成再滚动
6.TabLayout 固定在顶部
实现:
1.参考上一个 Demo
2.给头像设置layout_anchor属性悬挂在某个控件上即可
3.给头像设置一个 Behavior,使起 DependentAppBarLayout ,然后 Scale 即可。
4.给两个 view 设置不同的layout_collapseParallaxMultiplier 系数即可,不知道这个属性的回头看我上一篇博客
5.给 RecycleView 设置AppBarLayout.ScrollingViewBehavior
6.可以放在 AppBarLayout 里面,layoutflag 不设置 scroll 属性即可。

具体实现请参考源码:传送门


美高梅手机游戏网站 125.gif5

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
    }

Demo3

美高梅手机游戏网站 13

Demo3.gif

传送门
哈哈,大家自己去看吧,这些效果的实现用到的知识点我的 MaterialDesign系列都讲过的。
另外,这几个 Demo 以及我文中的那几个 Demo,大家记得都去敲一遍代码,敲了一篇就会了,真的,不骗你,


2.应用场景:

这是商品信息的布局文件,RelativeLayout 宽度一定要 match_parent,因为这样才能适应 GirdLayout 在设置列数时能完全占满宽度。SquareView 是重写 onMeasure() 的 ImageView ,由于重写了计算长度的方法,所以 layout_height 可以设置为 0dp ,减少绘制。

有话说

MaterialDesign 系列到这里就结束了,以后有时间再补充一下UI 篇吧。不过我这方面我也还是一知半懂的状态,等以后有心得了会动笔的,这里我推荐扔物线的HenCoder,我也在跟着学,业界良心之作,看完之后再也跟自定义 View 说 “No”,产品再也不会说“ 为什么 iOS 都实现了你们 Android 实现不了”。

演示常用的几种:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

   <com.example.administrator.animationview.SquareView
       android:id="@+id/show_iv_showimg"
       android:layout_width="match_parent"
       android:layout_height="0dp" />

    <LinearLayout
        android:id="@+id/linearLayout_2"
        android:layout_below="@+id/show_iv_showimg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp">
        <TextView
            android:layout_weight="6"
            android:id="@+id/show_service_tv_serviceNick_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"/>
        <TextView
            android:gravity="end"
            android:layout_weight="4"
            android:id="@+id/show_service_tv_servicePrice_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"/>
    </LinearLayout>
</RelativeLayout>

搜索历史:标题文本可以根据传入的标题layout文件定制

  1. 在动态添加 view 的过程, 在 onBindViewHolder() 这个方法中动态添加 view 。

美高梅手机游戏网站 146-gif6

筛选条件:设置多个FlowView即可

private void addView(ViewHolder viewHolder, final List<MessageBean> messageBeans){
        viewHolder.showMessage.removeAllViews();
        final int columcounts = viewHolder.showMessage.getColumnCount();
        int marginlength = DipUtils.dipTopx(context,6);

        for(int i=0;i<messageBeans.size();i++){
            Log.d("ooo",messageBeans.size()+"");
            GridLayout.Spec rowSpec =  GridLayout.spec(i/columcounts);
            GridLayout.Spec columSpec = GridLayout.spec(i%columcounts,1.0f);

            View view = LayoutInflater.from(viewHolder.showMessage.getContext()).inflate(R.layout.item_gl_show,viewHolder.showMessage,false);
            SquareView squareView = view.findViewById(R.id.show_iv_showimg);
            TextView  nick = view.findViewById(R.id.show_service_tv_serviceNick_2);
            TextView  price = view.findViewById(R.id.show_service_tv_servicePrice_2);
            squareView.setImageResource(messageBeans.get(i).getImg());
            nick.setText(messageBeans.get(i).getNick());
            price.setText(messageBeans.get(i).getPrice());


            final int finalI1 = i;
            squareView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(context,messageBeans.get(finalI1).getNick()+ finalI1,Toast.LENGTH_SHORT).show();
                }
            });

            GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(new ViewGroup.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT));
            layoutParams.rowSpec=rowSpec;
            layoutParams.columnSpec=columSpec;
            layoutParams.setMargins(marginlength, marginlength, marginlength, marginlength);
            viewHolder.showMessage.addView(view, layoutParams);
        }

网格线:支持实线,虚线

在这段代码中,getColumnCount() 方法就是获取 xml 文件中 GirdLayout 设置的列数。而其中的 dipTopx() 这个方法是 dp 转化为像素的方法 ,主要是为了之后 GirdLayout 设置 margin 参数 。关于 dp ,px,sp 区别与联系 这篇文章详细讲解,不再细说。
http://blog.csdn.net/zhoujiyu123/article/details/54407719

美高梅手机游戏网站 157.gif7

然后就是 GirdLayout 的 rowSpec/columnSpec 分别表示的是行/列配置对象,这里分别用(除/模)得到子控件的(行/列)索引值。GridLyout 设置需要 LayoutParams 对象 。这里还涉及一个知识点就是 LayoutInflater.inflate 参数问题:
下面链接提供详细参考:
http://blog.csdn.net/u012702547/article/details/52628453
http://blog.csdn.net/runningampH/article/details/51003264

多级菜单:每一级数据由一个FlowView组成

总的来说假如是
inflate(@LayoutRes int resource, @Nullable ViewGroup root,boolean attachToRoot)
三个参数。

添加/点击动画:动画支持自定义

  • 假如 root 不为 null ,且 attachToRoot 为 true 的话,那么在父布局 root 中将会添加-这个 resource ,不用再 root.addView() 就已经有该布局。

  • 假如 root 不为 null ,且 attachToRoot 为 false ,那么不会将 resource 添加进去 。但是 resource 中的根布局的参数将会有效,因为 layout_width ,layout_height 都是依赖父布局而存在的。

  • 假如 root 为null,不论 attachToRoot 为 true 还是为 false,显示效果都是一样的。当 root 为 null 表示我不需要将第一个参数所指定的布局添加到任何容器中,同时也表示没有任何容器来来协助第一个参数所指定布局的根节点生成布局参数。那么系统就会为我们默认生成一个LayoutParams,通过 generateDefaultLayoutParams() 方法来生成。

美高梅手机游戏网站 168. gif8

标签控件-FlowView

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

特性

而对于两种参数:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)

目前标签只支持TextView及其子类,FlowView内部的每个标签便是一个TextView,通过layout文件生成,所以标签本身定制很自由,此外还支持以下特性:

  • root 不为 null ,则与 attachRoot 为 true 情况相同。
  • root 为 null , attchRoot 为 false 或 true 都无关。

1.支持标题,全选,反选,关闭,确认五种功能标签,基本可以自由组合.

2.支持普通展示标签,热门标签 单选标签,多选标签.

3.支持填充(排满一行后自动递增行数)和网格(和GridView类似)两种排列模型.

4.支持指定行数或指定列数,指定FlowView宽度,配合HorizionScrollView可以水平滑动.

5.支持添加网格线,分实线和虚线,垂直/水平,粗细,宽度等均可调整.

6.支持限定行数,多余行数会舍弃(和上面的指定行数不是一个特性,指定行数不会舍弃标签).

7.支持自定义标签的添加动画和点击动画,自带随机,延迟,抖动动画.

基本上,只有文本展示的有限标签列表,不再需要RecyclerView,GrilView,ListView和Adapter配合,只需要提供标签的layout文件和一组String集合即可展示.

美高梅手机游戏网站,导入

美高梅手机游戏网站 17compile

自定义属性,按类型分组介绍:

标签属性,这些属性必须设置,除非只使用普通展示标签,否则请务必在xml中设置或在添加标签前调用setAttr系列方法设置,建议在xml中设置.

美高梅手机游戏网站 1810-attr1

标签常规设置属性,这些不是非必须属性,fv_select_type属性不需要设置,只要调用相关的add系列方法(下文会介绍该系列方法),FlowView便会自动设置 对应的值给select_type.

美高梅手机游戏网站 1911-attr2

至于为什么该属性设置无意义,下文会说明.

美高梅手机游戏网站 2012-attr3

手机美高梅游戏网址,指定列数和行数,水平滑动属性

如果需要FlowView展示成类似ListView的单行布局或者GridView的网格布局,请使用以下属性

美高梅手机游戏网站 2113-attr4

网格模式属性

这些属性类似ListView和GridView中的分隔线,可以根据自己项目的需求来使用

美高梅手机游戏网站 2214-attr5

注:fv_hLineAllCover 和 fv_VlineAllCover属性

fv_hLineAllCover 和 fv_VlineAllCover均为false时,显示效果如下图

如果使用到了标题标签,可以通过设置firstHLine和titleAllCoverVLine属性来显示或隐藏第一行的横线和竖线,到达自己想要的效果.

美高梅手机游戏网站 2315- cell1

fv_hLineAllCover 和 fv_VlineAllCover均为true时,显示效果下图

美高梅手机游戏网站 2416 -cell2

以上的自定义属性除了部分声明之外,都可以在xml中设置,但添加标签数据的add系列方法必须在代码中调用.

在代码中设置,先看add系列方法:

编辑:美高梅手机游戏网站 本文来源:发现这东西其实也没那么复杂,况且在相互嵌套

关键词: