Android自己定义view之measure、layout、draw三大流程
自己定义view之measure、layout、draw三大流程
一个view要显示出来。须要经过測量、布局和绘制这三个过程,本章就这三个流程具体探讨一下。View的三大流程具体分析起来比較复杂,本文不会从根源具体地分析,可是能够保证能达到实用的地步。
1. measure过程
1.1 理解MeasureSpec
View的測量方法为public final void measure(int widthMeasureSpec, int heightMeasureSpec)
和protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
,它们的參数都是两个MeasuerSpec。因此在搞懂測量过程之前。要先搞明确MeasureSpec。
MeasureSpec能够理解为測量规范。它是一个32位的int值,高2位代表SpecMode。低30位代表SpecSize。SpecSize就代表測量值。查看MeasureSpec的部分源代码,不难发现MeasureSpec的工作方式。
MeaureSpec部分源代码:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
不难看出MeasureSpec的结构是怎样的。而且我们能够从一个MeasureSpec中分解出SpecMode和SpecSize,也能够用SpecMode和SpecSize来组装一个MeasureSpec。
在上面的代码中看到了有SpecMode,我们先看下SpecMode的源代码和凝视。
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
SpecMode有三种:
1. EXACTLY:确定大小。假设SpecMode是Exactly,那么SpecSize是多少,測量结果就是多少。
比方子View在layout中设置的是指定大小。那么在測量时从父布局中传到measure方法的SpecMode就是Exactly。
或者说子布局设置的是match_parent,而父布局此时已经能够确定自己的大小,那么模式也是Exactly。
2. AT_MOST:子布局能够自行确定自己的大小。可是不能超过SpecSize的大小。典型的就是父布局已经确定了自己的大小,而子布局设置的參数是wrap_content,
3. UNSPECIFIED:父布局对子view不做限制,要多大给多大。用于特殊的測量场合。
1.2 理解measure过程
在弄明确MeasureSpec之后,就能够看看measure方法了。事实上measure方法是View中的一个final方法,我们是无法重写的。measure方法做了一些基本工作。可是在measure方法的凝视里说道真正的measure工作应该放在onMeasure方法里。所以基本都是重写onMeasure方法。
接下来看一下onMeasure的源代码和凝视。大家在看源代码的时候千万不要漏掉凝视仅仅看源代码。凝视是非常重要的。往往一些流程的说明就在凝视里,看关键凝视能比看十个方法的源代码实用。
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
凝视里的第一部分说了,測量大小的工作应该主要放在这种方法里,而且全部继承的子类都应该重写这种方法。
还有两个比較重要的点:一是重写的方法在onMeasure最后一定要将測量结果通过setMeasuredDimension(int, int)
来存储起来,这样这个view的measuredWidth和measuredHeight就是有效值了;二是须要保证測量得出的高和宽不能小于getSuggestedMinimumHeight()
和getSuggestedMinimumWidth()
的返回值。
View类中的onMeasure方法非常easy,它就是直接获得建议的宽和高作为測量结果。为了能明确onMeasure方法而又不至于陷入源代码无法自拔,我们选取一个比較简单的FrameLayout来看看它的onMeasure源代码,相应的凝视我已经写在里面了。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
/**
* 来确定是否须要反复測量那些宽和高參数为match_parent的子view,假设FrameLayout的宽高都不是确定的(Exactly),
* 那么仅仅有在确定了FrameLayout的宽高之后。才干去測量那些宽或高參数为match_parent的子view。
* */
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
/**
* 測量全部可见的子view
* */
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
/**
* 这个是測量子view的主要方法
* */
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
/**
* 子view測量完之后,获取子view的測量的宽和高,然后用FrameLayout已有的长和宽相比較,取其大者,这样能保证
* 完整显示全部的子view。
* */
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
/**
* 将padding也加入到測量结果中
* */
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
/**
* 检查一下是否小于推荐的最小值,假设小于了。就使用推荐的最小值
* */
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
/**
* 再比較前景的大小,取其大
* */
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
/**
* 设置该FrameLayout的測量大小
* */
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
/**
* 查看须要測量宽或高为match_parent的子view,假设须要測量,就又一次构造子view的MeasureSpec。
* */
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT) {
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
lp.leftMargin - lp.rightMargin,
MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
lp.topMargin - lp.bottomMargin,
MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
上面就是FrameLayout的測量过程,结合凝视能看得非常明确。
首先FrameLayout会測量全部的子view。假设FrameLayout的大小是确定的,那么一轮測量就能够确定全部子view的大小。假设FrameLayout的大小不确定,比方设置为wrap_content,那么此时那些宽或高參数为match_parent的子view是无法被确切測量大小的。因为此时parent的大小都还不知道呢,而且这些子view会存到mMatchParentChildren
里。
一轮測量下来。此时FrameLayout的宽和高分别都是測量过的子view的最大的宽和最大的高(最大的宽和最大的高不一定会出如今同一个子view上)。
为防止出现极端情况。比方全部的子view宽高參数都是match_parent,那么此时測量出来的宽和高都是0(为什么是0后面会解释)。
因此还须要对照一下最小的建议值以及前景的宽和高。
最后一轮是測量那些宽或高參数为match_parent的子view,此时FrameLayout的大小已经确定了,然后使用FrameLayout的SpecMode和第一轮刚測量出来的宽高又一次构造子view的MeasureSpec,然后再又一次測量。
至于父layout怎么測量子view的。事实上从onMeasure
的第二轮測量中就能够看到。首先父layout会依据自己的MeasureSpec和要測量的子view的LayoutParams.width、LayoutParams.height来生成子view的MeasureSpec。然后将这个MeasureSpec传给子view的measure方法,子view再依据自己的情况来測量自己大小。在父layout生成子view的MeasureSpec的过程中。主要是getChildMeasureSpec
方法,浏览一下这种方法的源代码。
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
方法并不难,我们能够非常清晰地看到父layout是怎样生产子view的MeasureSpec的。各项的凝视也写得非常清晰,总结起来例如以下表:
这个非常easy,能够看到父layout是怎样依据自己的MeasureSpec和子view的LayoutParams来创建子view的MeasureSpec的。
在onMeasure的第一轮測量中有个比較关键的方法measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
,从名字能够看出这种方法是測量子view的。而且还会将子view的margin值考虑在内。我们能够接着看下这种方法的源代码。
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
能够看出这种方法仍然是先生成了子view的MeasureSpec,然后调用子view的measure方法。须要注意的是传入的各个參数。查看getChildMeasureSpec(int spec, int padding, int childDimension)
方法。我们知道它的三个參数分别为:
- int spec:该view的MeasureSpec。这里就是FrameLayout的Spec。
- int padding:这并非单纯字面意思的父layout的padding值,简而言之,这就是父layout已经被占用掉的空间。假设要生成的是HeightMeasureSpec,那么这个padding就包含其它子view在垂直方向已经占用掉的位置、父layout的paddingTop和paddingBottom、子view本身的topMargin和bottomMargin值。
- int childDimension:要获取MeasureSpec的子view的宽度或者高度值。这个值是确定大小、wrap_content和match_parent这三种情况之中的一个。
在onMeasure
方法中第一轮測量子view的写法是
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
显然传给这种方法的widthUsed和heightUsed值是0,到这种方法内部的getChildMeasureSpec(int spec, int padding, int childDimension)
中的padding就不包含其它子view已经占用掉的位置。
这也恰好符合FrameLayout的特点:全部的子view都重叠排列,互相之间不影响。
而且Framlayout假设是wrap_content的,那么它的宽是全部子view中最大的宽、高是全部子view中最大的高。
1.3 measure过程总结
以上就是FrameLayout完整的測量过程,当然FrameLayout本身布局特点就是非常easy的,假设是RelativeLayout,那么測量过程会更加复杂。
但即使如此。我们还是能从中总结出Layout測量的一般规律的:
- 通过自身的MeasureSpec和子view的LayuoutParams。生成子view的MeasureSpec。这一步调用的是
getChildMeasureSpec(int spec, int padding, int childDimension)
方法。 - 调用子view的
measure(int widthMeasureSpec, int heightMeasureSpec)
方法,来測量子view的宽高。 - 在子view測量结束之后,依据情况来计算自身的宽高。假如自己的MeasureSpec是Exactly的。那么能够直接将SpecSize中的大小作为自己的宽或高;假设是wrap_content或者其它的,那么就须要在每个子view測量完之后。调用子view的
getMeasuredHeight()
和getMeasuredWidth()
来获得子view測量的结果,然后依据情况计算自己的宽高。 - 使用
setMeasuredDimension(int measuredWidth, int measuredHeight)
方法保存測量的结果。
这样就完毕了layout及其子view的測量过程。而view的測量就更简单了,因为没有子view。仅仅要确定了自身内容的大小。再结合MeasureSpec便能够測量完毕。
比方有个view须要画个圆,那么仅仅要考虑设置的padding值以及圆的大小就可以。
注意:三种宽、高值的差别:
1. LayoutParams.width、LayoutParams.height:这个是布局文件里的宽度和高度值,单位是px。而且WRAP_CONTENT相应-2。MATHCH_PARENT相应-1,这个是不论什么时候都能够调用的。特别注意,不管是measure还是layout过程,都不会对这个LayoutParams产生影响,除非在代码中手动调用setLayoutParams()方法来设置,否则LayoutParams中存储的都是布局文件里的宽和高。既不是測量出来的宽和高。也不是终于确定的宽和高。能够做个实验,初始化随意一个控件时。使用ViewTreeObserver
加入ViewTreeObserver.OnGlobalLayoutListener
,并在回调函数中打印log来分别显示使用LayoutParams获取控件宽和高,以及直接使用getWidth()和getHeight()来获取宽和高,就能够看到效果。从这方面来说,事实上LayoutParams更像是一种基准,是给父layout为该view生成MeasureSpec时參考用的。而不一定确切就是当中的值。所以假设要获得一个控件的真实宽和高。一定不要使用LayoutParams。能够通过setLayoutPramas(LayoutParams param)
方法来改变view的宽和高,而且往往这是唯一能够手动指定宽和高的方法,比方尽管TextView有setWidth(int width)
方法,可是ImageView却没有。仅仅能通过setLayoutPramas(LayoutParams param)
。2. getMeasuredWidth()、getMeasuredHeight():获取測量出的宽和高。这是在measure方法结束后才干够得到有效值。
3. getWidth()、getHeight():获取终于实际的宽和高。实际的宽和高在layout阶段才会确定,可是大部分情况。測量出的宽和高就是终于的宽和高。
2. layout过程
layout相比measure,就比較简单,而且不像measure是有固定套路,基本实现方式比較自由。
和measure同理。在自己定义view的时候应该重写onLayout()
,尽管layout()
方法是能够重写的。
2.1 理解layout过程
相同为了简单。我们继续选取FrameLayout的layout过程来学习。或许大家会说,FrameLayout的布局太简单了,不就是全部view都靠着左上角布局吗?事实上并非这么简单的。FrameLayout也有能够控制子view位置的參数,而且在布局过程中我们会看到某些须要在自己定义view中须要注意的事情。接下来就看一下layout函数。须要注意的是layout函数是在View类中定义的,而且FrameLayout遵循了规范,并没有重写layout函数。因此须要到View类中找到layout函数。
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
能够看到,View的layout函数事实上并没有运行多少实际的布局操作。而是负责一些状态的更新以及本view的坐标设置。使用setFrame()
函数将父容器传来的坐标应用到了自己的视图。然后调用了onLayout(changed, l, t, r, b)
,接下来就能够去看FrameLayout的onLayout()
函数了。须要注意的是。l、t、r、b都是相对于父view的位置,而不是在屏幕中的绝对位置,这点在凝视里也说明了。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
非常easy。直接调用了layoutChildren
,而且比原来的參数多了个布尔值。
当中changed
这个參数须要说明一下。这是布局依据自己原来的位置和之后layout函数中传来的新值做比較来发现自己的大小和位置是否改变,假设改变则changed为真,否则为假。这也能够用来决定什么时候对子view进行layout操作。避免频繁进行不必要的layout浪费资源。而验证是否changed这项工作是有View类的layout函数中进行的,因此不必我们操心。
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
/**
* 获取父layout的上下左右位置,这里的父layout是指FrameLayout本身
* */
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
mForegroundBoundsChanged = true;
/**
* 依次測量每个子view,而且考虑子view的gravity
* */
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
/**
* 依据gravity和布局方向来确定横向和纵向的绝对gravity,以此来决定子view的左边和上边的位置。
假设熟悉开发人员选项,
* 会发现当中有一个选项是“强制从右到左的布局”,而且FrameLayout也有android:layoutDirection属性,能够设置为继承(inherit)、
* 本地(local)、从左到右(ltr)、从右到左(rtl)四个选项,可是没有设置上下方向的,毕竟没有哪个文化的阅读习惯是上下颠倒的。因此
* 左右的gravity须要结合布局方向,可是上下布局仅仅须要解析view自己的gravity设置就可以,而不须要direction。
另外absoluteGravity
* 是依照从左到右的,不管开发人员选项里怎么设置。
* */
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
/**
* 计算子view的left位置
* */
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
/**
* 假设绝对gravity为横向居中。以下的计算显而易见是为了让子view横向居中的。而且考虑了margin值
* */
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
/**
* 假设不是强制gravity为左,那么还要计算gravity为右的情况
* */
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
/**
* 计算子view的top位置
* */
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
/**
* 横向居中布局
* */
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
/**
* 调用子view的layout函数,传入计算好的子view的left、top、right、bottom值
* */
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
关键凝视已经在代码中写好了,我们能够从中发现,onLayout的关键就是在于确定子view的上下左右四个边界的位置。
而FrameLayout的布局过程中。全部子view的布局都是独立的,而不会受到其它子view的影响,这也验证了FrameLayout的特性:全部子view都会叠加排列。假设是竖向布局的LinearLayout。那么每次下一个子view的top都会建立在上一个子view的bottom位置的基础上来计算,以保证它们是顺序排列的。这一点能够自己查看LinearLayout的onLayout()
函数验证。
或许有人会问。这仅仅是測量了位置,可是还没有应用到视图啊。
事实上设置不在onLayout()
函数,而是早在View的layout()
函数中就进行了。就是setFrame()
函数,changed就是setFrame()
函数的返回值。从这里也能够看出,子view的位置(应该)全然是由父layout确定的。而且在父layout调用子view的layout()
函数中直接设置了位置。不建议强行在onLayout()
函数中再次调用setFrame()
。避免出现布局错乱。onLayout()
函数应该仅仅用来布局子view,或者进行其它须要在layout阶段进行的工作。比方打log。
假设自己定义view是一个view而不是layout。那么全然不用重写onLayout()
也是能够的。
2.2 FrameLayout属性验证
在布局过程中看到。在不改变FrameLayout的布局方向的情况下(毕竟改变布局方向的情况非常少)。仅仅有子view的gravity和margin值能影响子view的位置。
接下来会验证一些特性。
(1) 无不论什么特殊设置
新建一个布局,没有不论什么特殊选项来看看效果
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.zu.customview.FrameLayoutTest">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"/>
</FrameLayout>
效果
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenVndW9ydWk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="FrameLayout-无特殊设置" title="">
(2) 子view加入margin
改动布局TextView,加入margin
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"
android:layout_marginLeft="20dp"
android:layout_marginTop="30dp"/>
效果
这里我们有个现象能够看一下,假设设置marginLeft值一直到TextView的右边界超出FrameLayout的右边界,会出现什么情况。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"
android:layout_marginTop="30dp"
android:layout_marginLeft="330dp"/>
我们发现TextView竟然折行了。显然子view是也能够得到父layout的布局信息的,而且在布局过程中会自己主动进行某些改变。尽管这一点来说是比較智能的,但不可避免的会出现某些不希望出现的情况。假设不希望出现子view自做主张的情况,在measure时能够构建一个UNSPECIFIED的MeasureSpec来測量子view。
(3) 子view加入layout_gravity
改动布局TextView。加入gravity
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="35sp"
android:textColor="#ff3467"
android:layout_gravity="center"/>
效果
(4) FrameLayout改动layoutDirection
去掉TextView的margin和gravity。然后在FrameLayout中加入以下一句。就能够改动FrameLayout的布局方向为从右到左
android:layoutDirection="rtl"
效果
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenVndW9ydWk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">
从以上过程,我想大伙儿应该已经全然能理解FrameLayout的布局过程,也顺便从代码中了解了一些FrameLayout其它的特性而且做了验证。
2.3 layout过程总结
layout的一般过程就是如此。总结起来就是例如以下几步:
- 父layout在自己的
onLayout()
函数中负责对子view进行布局,安排子view的位置,而且将測量好的位置(上下左右位置)传给子view的layout()
函数。 - 子view在自己的
layout()
函数中使用setFrame()
函数将位置应用到视图上,而且将新位置和旧位置比較来得出自己的位置和大小是否发生了变化(changed)。之后再调用onLayout()
回调函数。 - 假设此时子view中还有其它view,那么就在自己的
onLayout()
函数中对自己的子view进行第1补的布局操作。如此循环。仅仅到最后的子view中没有其它view。这样就完毕了全部view的布局。
当然。以上说的还是ViewGroup的layout过程,假设是View的layout过程就会更加简单,毕竟没有子view,仅仅要将传进来的位置应用到视图上就OK。
3. draw过程
draw过程相比与其它的两个就简单多了。它的作用就是把view内容绘制到屏幕上。
先看一下View的draw()
源代码
/**
* Manually render this view (and all of its children) to the given Canvas.
* The view must have already done a full layout before this function is
* called. When implementing a view, implement
* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
* If you do need to override this method, call the superclass version.
*
* @param canvas The Canvas to which the View is rendered.
*/
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
...
}
能够看出绘制分为6步,当中第二步和第五步通常都是跳过的。
我们仅仅看剩下的四步:
- 绘制背景
- 绘制自己的内容(onDraw())
- 绘制子view(dispatchDraw())
- 绘制装饰
以上步骤不难理解,自己在Bitmap画过自己定义图形的同学都知道。canvas绘图,假设位置重叠的话,后绘制的内容会把先前的内容覆盖掉。这个canvas是由ViewRoot传过来的。这样保证界面上全部的子view都绘制在一张画布上。事实上全部view的測量、布局、绘制过程都是由ViewRoot发起的。
凝视里仍然说明了draw()
方法不应该被重写,应该在onDraw()
方法里处理本身的绘制,而在dispatchDraw()
里绘制子view。假设一定要重写draw()
方法,那么也一定要在開始调用super.draw(Canvas canvas)
。
因为不同的view绘制方法不同。而且有的是layout有的是view。对于是否须要绘制子view的需求也不同,所以View类中的onDraw()
和dispatchDraw()
都是空实现。而因为ViewGroup是容器,自身须要绘制的东西比較少,主要在于子view的绘制,因此ViewGroup主要实现了dispatchDraw()
。相反的,TextView、ImageView等这些非容器的控件则主要实现onDraw()
方法来呈现更为复杂的内容。
本来流程并非非常复杂,相比起流程,draw过程的细节却多得让人可怕。只是为了全然弄明确容器和控件的绘制过程。我们仍然节选一点源代码简单了解一下。
节选一段ViewGroup类中的dispatchDraw()
方法
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
if (buildCache) {
child.buildDrawingCache(true);
}
}
}
}
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
controller.start();
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
int clipSaveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save();
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
if (preorderedList != null) preorderedList.clear();
...
}
看得出来非常多工作是为绘制子view做准备的,包含准备cache、依据clip来设置canvas等,真正绘制的语句是more |= drawChild(canvas, child, drawingTime)
这句。我们看下这个函数的源代码。
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
从凝视中能够看到,这种方法不仅仅是为了绘制内容这么简单,我们对View应用的各种动画或者其它视觉效果也将在这里体现。它直接调用了子view的draw()
,但显然这个draw函数和我们之前提到的不一样,如今这个三个參数。
这个函数仍然是在View类中,因为源代码太长,我就不贴了。
凝视中说明了。这个是专门让ViewGroup.drawChild()
来调用的,子类不应该重写这种方法,也不应该在除了ViewGroup.drawChild()
之外的其它地方调用它。
看一下它的代码,会发现里面是一些对canvas进行位移、缩放和变形的代码。也验证了它确实是为View的动画效果准备的。
而且在这里也调用了单參数版本号的draw(Canvas canvas)
来绘制内容。
具体各View怎么绘制的,就不具体看了,canvas本来就有绘制各种图像的方法,比方绘制椭圆、方形、文字、甚至还有绘制Drawable方法。
在查看了TextView和ImageView的onDraw()
方法后。发如今正式绘制前。都会把内容绘制到一个Drawable上,然后再将这个Drawable绘制到canvas上。
至于Canvas、Paint、Path、Drawable等等这些和图像相关的,能够具体去查一下。现学现用也能够。
总结
至此View的显示流程就解说完毕了,也看到了一个View走过了measure、layout和draw这三大阶段须要多么复杂的工作,在此不得不感叹一句:如今的cpu真tm快啊~那么多view那么多流程,还能保证每秒60fps的帧率。
有点跑题。总之具体内容在各部分也已经讲的非常明确了。这里仅仅做一个简单的总结。
measure
- measure过程的信息传递是基于MeasureSpec的。
- MeasureSpec由SpecMode和SpecSize组成。
SpecMode有三种:EXACTLY、AT_MOST、UNSPECIFIED。
- 子view的MeasureSpec是由父容器结合自己的MeasureSpec和子view的LayoutParams来构建的。所以在子view自己的measure函数中不必再考虑自己的LayoutParams。仅參考父容器传入的MeasureSpec就可以。
- 控件仅測量自己就可以。容器则须要測量子view。通过调用一个View或ViewGroup的
measure(int, int)
函数来測量。 - 測量完毕后务必调用
setMeasuredDimension(int, int)
来保存測量结果。 - 測量出的宽和高在绝大多数情况下是等于终于的宽和高的,可是不排除会不同。毕竟终于的宽和高是在layout阶段确定的。
layout
- 通过调用一个View对象的
layout(int, int, int, int)
函数对该view进行布局工作。 - 子view的确切布局是父容器负责确定的。父容器一旦调用子view的
layout(int, int, int, int)
,子view就会调用setFrame()
函数将位置应用到视图。而且依据旧位置和新位置来推断是否布局发生改变。并将推断结果和位置參数一起传到回调函数onLayout()
。 - 我们自己自己定义的布局工作应该都在
onLayout()
中完毕,包含对子view进行布局。
draw
这个应该是流程最简单可是细节最复杂的一步了。
具体的绘制须要用到Cavans、Drawable等,能够具体去查这方面的资料。
总之中的一个句话,内容都是绘制在传入的Canvas上,具体画什么怎么画,全然没有限制。
至此,View的显示的流程已经探索完了,接下来就是View的事件分发机制了,将会在我的下一篇博客《自己定义view之view事件分发机制》中解说。
声明:本系列文章部分知识点来自于《Android开发艺术探索》。在此对作者表示感谢。
部分内容可能会有错误和遗漏,欢迎大家留言讨论。
Android自己定义view之measure、layout、draw三大流程的更多相关文章
- Android应用层View绘制流程之measure,layout,draw三步曲
概述 上一篇博文对DecorView和ViewRootImpl的关系进行了剖析,这篇文章主要是来剖析View绘制的三个基本流程:measure,layout,draw.仅仅有把这三个基本流程搞清楚了, ...
- [Android学习笔记]View的measure过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- Android 自己定义View须要重写ondraw()等方法
Android 自己定义View须要重写ondraw()等方法.这篇博客给大家说说自己定义View的写法,须要我们继承View,然后重写一些 方法,方法多多,看你须要什么方法 首先写一个自己定义的V ...
- 【Android自己定义View实战】之自己定义超简单SearchView搜索框
[Android自己定义View实战]之自己定义超简单SearchView搜索框 这篇文章是对之前文章的翻新,至于为什么我要又一次改动这篇文章?原因例如以下 1.有人举报我抄袭,原文链接:http:/ ...
- Android 自己定义View (二) 进阶
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24300125 继续自己定义View之旅.前面已经介绍过一个自己定义View的基础 ...
- Android 自己定义View学习(2)
上一篇学习了基本使用方法,今天学一下略微复杂一点的.先看一下效果图 为了完毕上面的效果还是要用到上一期开头的四步 1,属性应该要有颜色,要有速度 <?xml version="1.0& ...
- Android自己定义View的实现方法
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17357967 不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了.回 ...
- 手把手带你画一个 时尚仪表盘 Android 自己定义View
拿到美工效果图.咱们程序猿就得画得一模一样. 为了不被老板喷,仅仅能多练啊. 听说你认为前面几篇都so easy,那今天就带你做个相对照较复杂的. 转载请注明出处:http://blog.csdn.n ...
- Android自己定义View基础篇(三)之SwitchButton开关
自己定义View基础篇(二) 自己定义View基础篇(一) 自己定义View原理 我在解说之前,先来看看效果图,有图有真相:(转换gif图片效果太差) 那来看看真实图片: 假设你要更改样式,请改动例如 ...
随机推荐
- jquery ready方法实现原理
先看这两句代码: window.addEventListener('load',loaded,false); document.addEventListener('DOMContentLoaded', ...
- Linux中使用GoAccess进行日志实时监控
一.用法命令: goaccess access_log -o /var/www/html/report.html --real-time-html 说明:请先安装Httpd和Goaccess 二.效果 ...
- ubuntu iptables设置【转】
root@qustdjx-K42JZ:/home/qustdjx# iptables -L -nChain INPUT (policy ACCEPT)target prot opt sourc ...
- H5 新增内容 新增属性
1.视频 video 2.音频 audio 3.拖放 Drag 和 drop 4.画布 canvas 5.SVG 6.地理定位 navigator.geolocation.getCurrentPosi ...
- Java HttpClient Basic Credential 认证
HttpClient client = factory.getHttpClient(); //or any method to get a client instance Credentials cr ...
- 小贝_mysql 触发器使用
触发器 简要 1.触发器基本概念 2.触发器语法及实战样例 3.before和after差别 一.触发器基本概念 1.一触即发 2.作用: 监视某种情况并触发某种操作 3.观察场景 一个电子商城: 商 ...
- 【转】UIAutomator源码分析之启动和运行
我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用AccessibilitService APIs来获取窗口界面控件信息已经注入用户行为事件,那么今天开始我们 ...
- Ubuntu下,terminal经常使用快捷键
# ctrl + l - 清屏 . cLear # ctrl + c - 终止命令. # ctrl + d - 退出 shell,好像也能够表示EOF. # ctrl + r - 从命令历史中找 . ...
- 代理Proxy初探
Proxy,也就是"代理"了. 意思就是.你不用去做,别人取代你去处理.比方说:租房.你仅仅要找到"我爱我家"中介,把全部的事情交给他们去代劳, "我 ...
- (转载)【TP5.0】设置session有效时长+修改默认存储路径
//查看默认session存储路径:print_r(session_save_path()); \thinkphp\helper.php if (!function_exists('ses ...