为什么学习

自从学了Android自定义控件的一些知识,总是处于似懂非懂状态,说都说了上来,自己在项目里封装了一些自定义控件,但是还是缺乏一个很直观的了解。所以去了解学习下Android是如何封装控件的,就从简单的入手,分析下LinearLayout是如何实现的

什么是LinearLayout

作为最基础的布局,所以从事过Android开发的同学都应该非常了解
中文解释应该叫做线性布局,相比如RelativeLayout,LinearLayout更简单,在没有weight的情况也每次只要测量一次就够,而RelativeLayout每次都需要测量两次

一些LinearLayout需要注意的属性

orientation 纵向排布或者水平排布

weight 权重,用于分配LinearLayout剩下的空间(会详细介绍)

measureWithLargestChild 这个属性不常见,如果赋值为true的话,所有
weight子View都会采用最大View的最小尺寸(为什么Android要设计这个属性,我也不是很理解)

源码分析

一般所有控件类的源码,都会从 measure, layout和draw3个方法入手,查看他们的回调函数onMeasure, onLayout和onDraw
只要明白这3个流程,一般控件的整个实现也就明白了
LinearLayout作为一个ViewGroup的子类,主要作为一个布局容器出现,所以我们需要重点查看写onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

从上面代码看到,LinearLayout的onMeasure方法实现非常简洁,根据布局方向分为measureVertical和measureHorizontal。下后面的onLayout和onDraw也是如此。鉴于内部实现基本一模一样,我在这只分析纵向的实现

/**
     * Measures the children when the orientation of this LinearLayout is set
     * to {@link #VERTICAL}.
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
     * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
     *
     * @see #getOrientation()
     * @see #setOrientation(int)
     * @see #onMeasure(int, int)
     */
    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        int maxWidth = 0;
        int childState = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;
        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;

        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // Optimization: don't bother measuring children who are going to use
                // leftover space. These views will get measured again down below if
                // there is any leftover space.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                int oldHeight = Integer.MIN_VALUE;

                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);

                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }

                final int childHeight = child.getMeasuredHeight();
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            /**
             * If applicable, compute the additional offset to the child's baseline
             * we'll need later when asked {@link #getBaseline}.
             */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds. If we skipped
        // measurement on any children, we need to measure them now.
        int delta = heightSize - mTotalLength;
        if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

                float childExtra = lp.weight;
                if (childExtra > 0) {
                    // Child said it could absorb extra space -- give him his share
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    delta -= share;

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // child has been previously measured
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above...
                        // base new measurement on stored values
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }

                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
                        // child was skipped in the loop above.
                        // Measure for this first time here      
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);


            // We have no limit, so make all weighted views as tall as the largest child.
            // Children will have already been measured once.
            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                for (int i = 0; i < count; i++) {
                    final View child = getVirtualChildAt(i);

                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }

                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();

                    float childExtra = lp.weight;
                    if (childExtra > 0) {
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                        MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight,
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
    }

整个方法代码不长,就300行左右,但是思路十分清晰 我们根据流程,一步一步来看

首先是初始化了一堆变量
我们挑几个重要的看

//记录内部使用的高度,别被字面意思误导了以为是LinearLayout的高度
 mTotalLength = 0;
 //权重值的总和
 float totalWeight = 0;
 //子view的数量,
 final int count = getVirtualChildCount();
 //其实调用的都是getChildCount(),外面套一层getVirtualChildCount()
 //可能是为了让读者更好的理解
 int getVirtualChildCount() {
        return getChildCount();
    }
 //LinearLayout的高度模式和宽度模式
 //如果这部分知识不理解的需要去看下Measure的过程      
 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
 final int heightMode=MeasureSpec.getMode(heightMeasureSpec);

初始化变量之后,就开始遍历所有子View了,然后对子View进行测量

             //首先把子View取出來
            final View child = getVirtualChildAt(i);
            //如果子View是null就继续测量下一个子View
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
            //如果子View是GONE的也不算在总高度里面,这里也能看出GONE和INVISIBLE的区别
            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }
            //如果有分割线,就把分割线高度加上
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

然后

//有时候我们在代码里面通过Inflater服务,动态加载一个布局,然后去设置他的LayoutParams,如果不引用父容器的LayoutParams就会报一个强转错误,原因就在这个 父容器在add,measure的时候都会把子View的LayoutParams强转成自己的类型
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

//计入总权重
totalWeight += lp.weight;

//这里就值得注意下了如果当前的LinearLayout是EXACTLY模式,且子view的高度为0,且权重大于0
//这个子view只有在LinearLayout高度有剩余的时候,才会根据权重的占比去平分剩余空间
//上文说的二次测量也就指的这部分
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
    // Optimization: don't bother measuring children who are going to use
    // leftover space. These views will get measured again down below if
    // there is any leftover space.
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
    skippedMeasure = true;
    }
//如果不是上述的情况
else {
    int oldHeight = Integer.MIN_VALUE;

        if (lp.height == 0 && lp.weight > 0) {
     // heightMode is either UNSPECIFIED or AT_MOST, and this
     // child wanted to stretch to fill available space.
     // Translate that to WRAP_CONTENT so that it does not end up
     // with a height of 0
     //这里其实官方的注释讲了也挺清楚的,到了这步,当前的LinearLayout的模式
     //肯定是UNSPECIFIED或者MOST,因为EXACTLY模式会进入上一个判断
     //然后把子View的高度赋值成-1(WRAP_CONTENT)
        oldHeight = 0;
        lp.height = LayoutParams.WRAP_CONTENT;
        }

     // Determine how big this child would like to be. If this or
    // previous children have given a weight, then we allow it to
   // use all available space (and we will shrink things later
  // if needed).
  //这里就开始测算子View了
  //如果当前的LinearLayout不是EXACTLY模式,且子View的weight大于0,优先会把当前LinearLayout的全部可用高度用于子View测量
        measureChildBeforeLayout(
          child, i, widthMeasureSpec, 0, heightMeasureSpec,
          totalWeight == 0 ? mTotalLength : 0);
// 重置子控件高度,然后进行精确赋值
        if (oldHeight != Integer.MIN_VALUE) {
            lp.height = oldHeight;
        }

       final int childHeight = child.getMeasuredHeight();
       final int totalLength = mTotalLength;
       //加上子View的margin值
       mTotalLength = Math.max(totalLength, totalLength+childHeight + lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));

      if (useLargestChild) {
           largestChildHeight = Math.max(childHeight, largestChildHeight);
           }
     }
//totalWeight == 0 ? mTotalLength : 0
//这里的totalHeight就是有这个决定的
//如果为0,就会把所有可用高度给子View
void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
    }

//接着看这个方法是如何实现的
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //这个就和普通的ViewGroup实现方法一样了,根据子View的LayoutParams和父容器的MeasureSpec共同决定了子View的大小
    //getChildMeasureSpec是属于ViewGroup的方法
    final int childWidthMeasureSpec = getChildMeasureSpec(
        parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight + lp.leftMargin +

    final int childHeightMeasureSpec getChildMeasureSpec(
        parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom + lp.topMargin
        + lp.bottomMargin + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
//在这两段代码之间还有些杂七杂八的处理,如果读者有兴趣可以自己阅读分析下
//当测量完子View的大小后,总高度会再加上padding的高度
// Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        //如果设置了minimumheight属性,会根据当前使用高度和最小高度进行比较
        //然后取两者中大的值
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds. If we skipped
        // measurement on any children, we need to measure them now.
        //到了这里,会再对带weight属性的子View进行一次测绘
        //首先计算属于高度
        int delta = heightSize - mTotalLength;
        if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
        //如果设置了weightSum就会使用你设置的weightSum,否则采用当前所有子View的权重和。所以如果要手动设置weightSum的时候,千万别计算错误哦
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;
        //这里的代码就和第一次测量很像了
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

                float childExtra = lp.weight;
                if (childExtra > 0) {
                    // Child said it could absorb extra space -- give him his share
                    //子控件的weight占比*剩余高度
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    //剩余高度减去分配出去的高度
                    delta -= share;

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // child has been previously measured
                    //如果是当前LinearLayout的模式是EXACTLY
                    //那么这个子View是没有被测量过的,就需要测量一次
                    //如果不是EXACTLY的,在第一次循环里就被测量一些了
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above...
                        // base new measurement on stored values
                        //如果是非EXACTLY模式下的子View就再加上
                        //weight分配占比*剩余高度
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }
                       //重新测量一次,因为高度发生了变化
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
                        // child was skipped in the loop above.
                        // Measure for this first time here
                        //如果是EXACTLY模式下的
                        //这里只会把weight占比所拥有的高度分配给你的子View
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }

                    // Child may now not fit in vertical dimension.
                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        }

上述就是onMeasure的主要分析

注意点
1.会根据当前LinearLayout的模式分成2条支线,如果是EXACTLY模式下的weight不为0,且高度设置为0的子View优先级是最低的。如果LinearLayout剩余空间不足,就会不显示
但是如果是AT_MOST的weight不为0,且高度设置为0就会优先获得高度
2.为LinearLayout动态添加子View的时候,子View的LayoutParams一定要是LinearLayout的内部类(适用于其他ViewGroup子类)

举个例子

所有的源码解析都是我们自己根据代码的推论,配合几个demo跑下会理解了更深

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_weight="2"
        android:background="@android:color/holo_orange_dark" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="fill_parent"
        android:background="@android:color/holo_blue_dark" />

</LinearLayout>

在分析之前大伙儿先预测下结果,思索下再往下看

假设我们的屏幕是1000dp的高度

父容器LinearLayout是EXACTLY模式,但是TextView1本身的height是300dp,所以会进入第一次测量
TextView1 在第一次测算时拿到了300dp的高度

然后TextView因为是match,所以会拿到1000dp的高度

然后由于有weight的子view,所以进入第二次测量

int delta = heightSize - mTotalLength;

delta = 1000 - 1300; // 结果是-300

int share = (int) (childExtra * delta / weightSum);

share = 2.0 * -300 / 2.0 //share的结果也是-300

MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTL);
所以最后TextView1的高度是0.不会显示在屏幕上,屏幕上应该被Textview2充满。
不信的话,大家可以试一下

02-10 15:42