編輯:關於Android編程
什麼是流式布局呢?也不知道哪個高手把它稱之為流失布局,叫什麼不重要,重要的是要知道怎麼實現,今天就實現下這個功能,先看下圖什麼就知道是什麼是流式布局了,做過電商的app或者網購的人都知道有一個什麼選擇規格(x,xl,ml)so,
當然這個用其他什麼gridview也能實現,如果大小是一樣的話,如果大小不一樣就不好搞定了,那麼如果使用今天講的流式布局就很好做了,那麼還是一開始並不是直接講這個效果怎麼實現,而是把相關的技術點盡自己的能力講清楚,如果這個懂了,說不定不僅這個流式布局懂了,也許你還懂了其他東西,這就是最好的,這就是為什麼不上來貼代碼的原因,而是花更多的時間把原理講清楚!要實現這個效果,就必須懂view的繪制流程,如圖:
這就是所謂的繪制流程三步驟,打個比方吧,你team叫你把一個控件放到手機屏幕上,那麼要問我要把一個多大的控件放在哪個位置啊,這裡就有二個詞很重要,多大,哪個位置,多大就是onMeasure(),哪個位置就是onLayout(),控件在屏幕上是顯示什麼,這就是內容了也就是onDraw(),
上面的圖說了onMeasure()方法也就是測量控件大小並不是最終的大小,又可能onLayout()方法中改變了view的大小,現在寫個小例子驗證下:
package com.example.flowlayout; import android.content.Context; import android.util.AttributeSet; import android.widget.LinearLayout; /** * Created by admin on 2016/6/13. */ public class MyLinearLayout extends LinearLayout { public MyLinearLayout(Context context) { this(context,null); } public MyLinearLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); } }效果:
你會發現textview寬和高就是包裹內容,我現在在onLayout()方法中添加幾行代碼:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); TextView tv = (TextView) getChildAt(0);//獲取MyLinearLayout控件的第一個子view,這個和xml布局是對應的 tv.layout(0,0,300,300); }效果圖:
看到textview的寬和高變成了300,300了吧,和之前的內容包裹是不是不一樣了,因為在onLayout()方法中改變了子view的寬和高,按到底這是違背view繪制流程的,但是可以這麼做,我們知道android view有二種,一種是view比如TextView,Button,ImageView,就是不能通過addView(View view)添加子view的,另外還有一種View是ViewGroup,就是存儲view的容器,但是ViewGroup是繼承自View,所以你也可以說android上所有的控件就一種View,
onMeasure()---測量
我們知道繪制流程第一步就是測量,從源碼中發現真正的測量是從measure()方法開始的,這個方法在view中而不是在ViewGroup中,所以剛才在自定義LinearLayout寫的onMeasure()方法也是繼承了View中的onMeasure()方法,那麼先看下View中的measure()方法:
* * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec);//重點 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }從上面方法中的注釋標記了紅色,意思是說水平和豎直空間需要父view提供,記住這個,往下會用到,從上面的measure()方法看到這是用final修飾的,表示子類不能繼承它,也就是說Google讓你不想打破它的測量框架,上面有一個很重要的方法onMeasure(widthMeasureSpec, heightMeasureSpec);一般測量都是繼承這個方法,onMeasure()源碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }其實onMeasure()方法中也就是調用了setMeasureDimension()方法,它也是接受2個形參,但是這二個形參確實調用了getDefaultSize()方法,
public static int getDefaultSize(int size, int measureSpec) { int result = size; //把size賦值給result int specMode = MeasureSpec.getMode(measureSpec);//獲取mode int specSize = MeasureSpec.getSize(measureSpec);//獲取size switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }從上面的形參的字面意思知道第一個形參是大小,第二個形參是測量規范,我是從字面意思翻譯的,因為spec是規范意思,
所以onMeasure()方法中的2個參數就不是一個具體的值,比如不是什麼100,200之類的,其實這100,200是由大小和規范決定的,現在看下getDefaultSize()方法,其中
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
MeasureSpec類源碼:
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }上面的幾個常量做一個簡單的介紹
UNSPECIFIED = 0 << MODE_SHIFT(=30)表示向左移30 最後的結果=0
EXACTLY = 1 << MODE_SHIFT表示左移30=1073741824
AT_MOST = 2 << MODE_SHIFT;表示左移30結果=-2147483648
MODE_MASK = 0x3 << MODE_SHIFT表示左移30結果-1073741824
現在看下getMode()的方法:
public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); }比如measureSpec=100,那麼getMode()最後返回的值為0,那麼就是UNSPECIFIED
public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }getSize()最後的返回的值就是measureSpec傳入的值,
結合上面2個方法以及getDefaultSize()我們總結一個結論
測量最終的值=size+mode
現在講下上面涉及到的三個變量也就是mode,
UNSPECIFIED:
表示視圖按照自己的意願設置成任意的大小,沒有任何限制,這個一般用在ScollerView上
EXACTLY
這個exactly是精確的意思,意思是說父view傳遞給子view的大小是精度的,那麼子view就應該接受父view傳遞給它的值是多少就是多少
AT_MOST
表示子view只能接受指定的大小,不能超過這個指定大小的范圍,就好像是LinearLayout的寬和高是100,而它的子view TextView只能接受最大值為100,不能超過這個100
現在看下之前自定義的LinearLayout中的onMeasure()方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int mode = MeasureSpec.getMode(widthMeasureSpec); Log.e(TAG,"mode------------------->"+mode); }log:
06-13 06:52:30.088 30004-30004/com.example.flowlayout E/MyLinearLayout: mode------------------->1073741824
把1073741824和上面的幾個分析的常量對比一下發現mode就是EXACTLY,哪為什麼是EXACTLY呢?看下布局文件:
發現MyLinearLayout寬和高都是match_parent,也就是填充父view的大小,它的父view就是RelativeLayout,而這個RelativeLayout的寬和高是讀取手機的屏幕賦值給RelativeLayout的,所以RelativeLayout的寬和高是一個定值,這就是為什麼mode為EXACTLY,如圖:
現在我把布局文件改變下,
現在打印下mode值為
06-13 07:22:11.258 23646-23646/com.example.flowlayout I/MyLinearLayout: mode------------------->-2147483648
這個是不是對應AT_MOST,因為LinearLayout的寬度為wrap_content,它的寬度取決於它孩子view的寬度,所以它不是固定的,那麼你MyLinearLayout就是最大取值反正不能超過父view的寬度就行,從上面的分析可以得出一般的結論:
1:當子view的寬和高設置為wrap_content,父view給它的mode為AT_MOST
2:當子view的高和寬設置為match_parent和確切的值的時候 父view給它的mode為EXACTLY
測量的最終是在setMeasuredDimension(int measuredWidth, int measuredHeight)方法中結束最後的測量過程,因為measuredWidth和measuredHeight都是最終的測量後的寬度和高度,從這個形參也知道後綴沒帶Spec這幾個字母,
在這裡我自定義一個View,
package com.example.flowlayout; import android.content.Context; import android.util.AttributeSet; import android.view.View; /** * Created by admin on 2016/6/13. */ public class MyView extends View { public MyView(Context context) { super(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { /** * 不調用父view的onMeasure()方法而是直接調用setMeasuredDimension() */ // super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(200,200); } }布局文件
我布局文件設置的寬和高都是50px,效果:
發現被騙了一樣,是的布局文件是不能當作最終的view的寬和高,是因為我們在MyView的onMeasure()方法中設置了
setMeasuredDimension(200,200);其實在ViewGroup類中還有一個測量子view的方法
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount;//子view的總數 final View[] children = mChildren;//記錄所有的子view(是一個數組) for (int i = 0; i < size; ++i) {//遍歷所有的子view final View child = children[i];//賦值某一個子view if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {//判斷這個View是不是Gone了也就是不可見 measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }現在看下measureChild()方法源碼:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }上面通過一系列對父view傳遞進來的寬和高計算,最終調用的是子view的measure()方法來最終測量寬和高
在這提一個知識點,就是getMeasuredWidth() getMeasuredHeight()這二個方法,我們只要看其中一個方法源碼就行
public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; }mMeasuredHeight這個值是在measure()方法中對進行賦值,而public static final int MEASURED_SIZE_MASK = 0x00ffffff;是一個定值,所以getMeasureHeight()方法是在測量後才能獲取到這個值,好了測量就講到這裡,現在接著講onLayout()方法
onLayout()
研究onLayout()方法首先要先研究下ViewGroup中的layout()方法開始
/** * {@inheritDoc} */ @Override public final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } super.layout(l, t, r, b);//調用父view的layout()方法也就是調用view的layout方法 } else { // record the fact that we noop'd it; request layout when transition finishes mLayoutCalledWhileSuppressed = true; } }
發現這個layout()方法也是final修飾的,所以子view不能繼承重寫這個layout()方法,現在看下view的layout()方法
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);//給繼承了ViewGroup的子類讓它自己去控制view的位置 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList看下onLayout()方法:listenersCopy = (ArrayList )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; }
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }發現它是一個空方法,哪好了畫圖理解下
其實view的layout的四個參數其實就是2個坐標點而已,如圖:
package com.example.flowlayout; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; import android.view.Window; import android.widget.Button; /** * Created by admin on 2016/6/13. */ public class MyView extends Button { private static final String TAG ="MyView" ; public MyView(Context context) { this(context,null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private float downX = 0; private float downY = 0; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); break; case MotionEvent.ACTION_MOVE: float moveX = event.getX(); float moveY = event.getY(); int l = getLeft(); int t = getTop(); int r = getRight(); int b = getBottom(); int newL = (int) (l+(moveX-downX)); int newT = (int) (t+(moveY-downY)); int newR = (int) (r+(moveX-downX)); int newB = (int) (b+(moveY-downY)); layout(newL,newT,newR,newB); downX = moveX; downY = moveY; break; case MotionEvent.ACTION_UP: break; } return true; } }這個是實現在屏幕上隨意拖動
現在講下View的getwidth()和getHeight()方法,直接上源碼
@ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; }widht=mRight-mLeft,從這個簡單的算法中就知道要想一個view通過getWidth()獲取到寬度,必須是onLayout()方法執行後
在這裡忘記了講下onLayout(l,t,r,b)方法中四個參數,其實就是離父view的left,top,right,bottom
布局文件:
在onLayout()方法中打印log;
06-13 12:50:17.293 11451-11451/com.example.flowlayout E/MyLinearLayout: l=10t=10r=110b=110
看出來了吧從log日記中,如圖:
好吧,onLayout()方法就講到這裡了,現在講一個例子引出另外一個技術點
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
>
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_height="wrap_content"
android:text="阿裡巴巴"
android:background="#ff0000"
android:padding="10dp"
/>
android:layout_height="wrap_content"
android:text="騰訊"
android:background="#ffff00"
android:padding="10dp"
/>
android:layout_height="wrap_content"
android:text="百度"
android:background="#00ff00"
android:padding="10dp"
/>
package com.example.measureviewdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class MyLinearLayout extends ViewGroup {
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLinearLayout(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
發現MyLinearLayout 是繼承了ViewGroup,裡面什麼邏輯代碼也沒寫,運行起來看有啥
發現叼都沒有,是因為沒有實現onMeasure()和onLayout()方法,因為我是繼承了ViewGroup,現在實現下這個二個方法中的邏輯
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height=0;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int count = getChildCount();//獲取所有的子view
for (int i=0;i
measureChild(view, widthMeasureSpec, heightMeasureSpec); //測量子view
int childWidth = view.getMeasuredWidth(); //測量後獲取子view的寬度
int childHeight = view.getMeasuredHeight();//測量後獲取子view的高度
//得到最大寬度,並且累加高度
height = childHeight;
width+= Math.max(childWidth, width);
}
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize: width, (heightMode == MeasureSpec.EXACTLY) ? heightSize: height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int viewWidth = 0;//記錄每個子view的寬度累加
for (int i=0;i
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
child.layout(viewWidth, 0, childWidth+viewWidth, childHeight);
viewWidth+=childWidth;
}
}
效果:
完成的把這三個子view顯示出來了,但是我現在布局文件中對這三個textview添加一個屬性android:layout_marginLeft="20px" 但是你發現運行起來的效果和上面的效果沒任何區別,按到底高度不變,寬度要加3*20也就是width+60呢?
本文講實現一個自定義列表的Android程序,程序將實現一個使用自定義的適配器(Adapter)綁定 數據,通過contextView.setTag綁定數據有按鈕的Lis
Android中經常會使用到Dialog(彈出框效果),而且功能非常強大,可以模擬出N種彈出框效果。如圖所示:下面將通過一個小實例,來像大家展示Android中功能強大的
手機能帶給你便捷、歡笑、感動,當然,也會帶給你小小的傷痛,比如,被偷、碎屏,或者掉進水裡——甭管這水是水池、下水道、馬桶還是火鍋,等
我認為在寫涉及到數據結構或算法的實現類的源碼解析博客時,不應該急於講它的使用或馬上展開對源碼的解析,而是要先交待一下這個數據結構或算法的資料,了解它的設計,再從它的設計