編輯:關於Android編程
本文主要介紹Android ViewGroup/View的繪制流程,及常用的自定義ViewGroup的方法。在此基礎上介紹動態控制View的位置的三種方法,並給出最佳的一種方法。
簡單的說一個View從無到有需要三個步驟,onMeasure、onLayout、onDraw,即測量大小、放置位置、繪制三個步驟。而ViewGroup的onMeasure、onLayout流程裡,又會遍歷每個孩子,並最終調到孩子的measure()、layout()函數裡。與View不同的是,ViewGroup沒有onDraw流程,但有dispatchDraw()流程,該函數最終又調用drawChild()繪制每個孩子,調每個孩子View的onDraw流程。
在onMeasure流程裡是為了獲得控件的高和寬,這塊有個getWidth()和getMeasuredWidth()的概念,前者指寬度,後者是測量寬度。一般來說,一個自定義VIewGroup(如繼承自RelativeLayout)一般要進兩次onMeasure,一次onLayout,一次drawChild()。雖然onMeasure流程是測量大小,且進了兩次。但直到最後一次出去的時候調用getWidth()得到的仍然是0.getWidth()的數值一直到onSizeChanged()的時候才能夠得到正確的,此後進到onLayout裡當然也能正常得到。
下面是我截的一段代碼:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub Log.i(TAG, "onMeasure enter..."); Log.i(TAG, "width = " + getWidth() + " height = " + getHeight()); Log.i(TAG, "MeasuredWidth = " + getMeasuredWidth() + " MeasuredHeight = " + getMeasuredHeight()); super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.i(TAG, "00000000000 width = " + getWidth() + " height = " + getHeight()); Log.i(TAG, "00000000000 MeasuredWidth = " + getMeasuredWidth() + " MeasuredHeight = " + getMeasuredHeight()); Log.i(TAG, "onMeasure exit..."); }
Line 355: 01-03 10:15:40.526 I/YanZi (10793): onMeasure enter... Line 357: 01-03 10:15:40.526 I/YanZi (10793): width = 0 height = 0 Line 359: 01-03 10:15:40.527 I/YanZi (10793): MeasuredWidth = 0 MeasuredHeight = 0 Line 361: 01-03 10:15:40.531 I/YanZi (10793): 00000000000 width = 0 height = 0 Line 363: 01-03 10:15:40.532 I/YanZi (10793): 00000000000 MeasuredWidth = 1080 MeasuredHeight = 1701 Line 365: 01-03 10:15:40.532 I/YanZi (10793): onMeasure exit... Line 367: 01-03 10:15:40.532 I/YanZi (10793): onMeasure enter... Line 369: 01-03 10:15:40.533 I/YanZi (10793): width = 0 height = 0 Line 371: 01-03 10:15:40.533 I/YanZi (10793): MeasuredWidth = 1080 MeasuredHeight = 1701 Line 373: 01-03 10:15:40.536 I/YanZi (10793): 00000000000 width = 0 height = 0 Line 375: 01-03 10:15:40.536 I/YanZi (10793): 00000000000 MeasuredWidth = 1080 MeasuredHeight = 1701 Line 377: 01-03 10:15:40.537 I/YanZi (10793): onMeasure exit... Line 379: 01-03 10:15:40.537 I/YanZi (10793): onSizeChanged enter... Line 381: 01-03 10:15:40.538 I/YanZi (10793): width = 1080 height = 1701 Line 383: 01-03 10:15:40.538 I/YanZi (10793): onSizeChanged exit... Line 385: 01-03 10:15:40.538 I/YanZi (10793): onLayout enter... Line 387: 01-03 10:15:40.539 I/YanZi (10793): width = 1080 height = 1701 Line 389: 01-03 10:15:40.540 I/YanZi (10793): onLayout exit...
至於為啥要進兩次onMeasure,翻遍了網絡麼有找到合理的解釋。有人說是大小發生變化時要進兩次,如Linearlayout裡設置了weight屬性,則第一次測量時得到一個大小,第二次測量時把weight加上得到最終的大小。可是我用Linearlayout把裡面所有的母和子的view大小都寫死,onMeasure還是進了兩次。RelativeLayout就不用說了也是進的兩次。國外文檔也有解釋說,當子view不能夠填滿父控件時,要第二次進到onMeasure裡。經我測試,貌似也是扯淡。我全都match_parent還是進了兩次。
當然在onMeasure裡可以直接setMeasuredDimension(measuredWidth, measuredHeight)設置控件寬和高,這樣不管xml裡咋寫的,最終以此句設置的width和height進行放置、顯示。關於View/ViewGroup繪制原理本文就介紹到這,更詳細請參考:鏈接1 鏈接2 鏈接3 鏈接4 都大同小異,可以看看。
方法一:
c_nanshi_guide.xml布局文件
可以看到布局裡並沒出現任何自定義信息。<frameLayout android:id="@+id/guide_nan_layout" android:layout_width="200dp" android:layout_height="150dp" android:background="@drawable/nan1" > </frameLayout>
NanShiGuide.java
package org.yanzi.ui; import org.yanzi.util.DisplayUtil; import android.R.color; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.RelativeLayout; import android.widget.TextView; import com.example.test1.R; public class NanShiGuide extends BaseGuideView { private static final String TAG = "YanZi"; int LAYOUT_ID = R.layout.c_nanshi_guide; View guideNanLayout; TextView guideNanText; private Drawable mDrawable; private Context mContext = null; public NanShiGuide(Context context, GuideViewCallback callback) { super(context, callback); // TODO Auto-generated constructor stub mContext = context; initView(); mDrawable = context.getResources().getDrawable(R.drawable.ong); } @Override protected void initView() { // TODO Auto-generated method stub Log.i(TAG, "NanShiGuide initView enter..."); View v = LayoutInflater.from(mContext).inflate(LAYOUT_ID, this, true); guideNanLayout = v.findViewById(R.id.guide_nan_layout); guideNanText = (TextView) v.findViewById(R.id.guide_nan_text); } @Override protected void onFinishInflate() { // TODO Auto-generated method stub Log.i(TAG, "onFinishInflate enter..."); super.onFinishInflate(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub Log.i(TAG, "onLayout enter..."); Log.i(TAG, "width = " + getWidth() + " height = " + getHeight()); int transX = 0; int transY = 0; if(mOrientation == 0){ guideNanLayout.setRotation(0); transX += 0; transY += 0; }else if(mOrientation == 270){ guideNanLayout.setRotation(90); transX += -DisplayUtil.dip2px(mContext, 25) + DisplayUtil.dip2px(mContext, 210); transY += DisplayUtil.dip2px(mContext, 25); }else if(mOrientation == 180){ guideNanLayout.setRotation(180); transX += DisplayUtil.dip2px(mContext, 160); transY += b - DisplayUtil.dip2px(mContext, 150); }else if(mOrientation == 90){ guideNanLayout.setRotation(270); transX += -DisplayUtil.dip2px(mContext, 25); transY += b - DisplayUtil.dip2px(mContext, 200 - 25); } guideNanLayout.setTranslationX(transX); guideNanLayout.setTranslationY(transY); // this.setTranslationX(transX); // this.setTranslationY(transY); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) guideNanLayout.getLayoutParams(); params.leftMargin = 100; params.topMargin = 100; guideNanLayout.setLayoutParams(params); super.onLayout(changed, l, t, r, b); Log.i(TAG, "onLayout exit..."); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub Log.i(TAG, "onMeasure enter..."); Log.i(TAG, "width = " + getWidth() + " height = " + getHeight()); Log.i(TAG, "MeasuredWidth = " + getMeasuredWidth() + " MeasuredHeight = " + getMeasuredHeight()); super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.i(TAG, "00000000000 width = " + getWidth() + " height = " + getHeight()); Log.i(TAG, "00000000000 MeasuredWidth = " + getMeasuredWidth() + " MeasuredHeight = " + getMeasuredHeight()); Log.i(TAG, "onMeasure exit..."); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // TODO Auto-generated method stub Log.i(TAG, "onSizeChanged enter..."); Log.i(TAG, "width = " + getWidth() + " height = " + getHeight()); super.onSizeChanged(w, h, oldw, oldh); Log.i(TAG, "onSizeChanged exit..."); } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub Log.i(TAG, "onDraw enter..."); super.onDraw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { // TODO Auto-generated method stub Log.i(TAG, "dispatchDraw enter..."); super.dispatchDraw(canvas); } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // TODO Auto-generated method stub Log.i(TAG, "drawChild enter..."); int w = getWidth(); int h = getHeight(); Point centerPoint = new Point(w / 2, h / 2); canvas.save(); mDrawable.setBounds(centerPoint.x - 150, centerPoint.y - 150, centerPoint.x + 150, centerPoint.y + 150); mDrawable.draw(canvas); canvas.restore(); return super.drawChild(canvas, child, drawingTime); } }
package org.yanzi.ui; import org.yanzi.util.OrientationUtil; import android.content.Context; import android.graphics.Canvas; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.RelativeLayout; import android.widget.TextView; public abstract class BaseGuideView extends RelativeLayout implements Rotatable, View.OnClickListener { protected int mOrientation = 0; protected Context mContext; private GuideViewCallback mGuideViewCallback; public interface GuideViewCallback{ public void onGuideViewClick(); } public BaseGuideView(Context context, GuideViewCallback callback) { super(context); // TODO Auto-generated constructor stub mContext = context; mGuideViewCallback = callback; setOnClickListener(this); mOrientation = OrientationUtil.getOrientation(); } @Override public void setOrientation(int orientation, boolean animation) { // TODO Auto-generated method stub mOrientation = orientation; requestLayout(); } protected abstract void initView(); @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub return true; //super.onInterceptTouchEvent(ev) } @Override public void onClick(View v) { // TODO Auto-generated method stub mGuideViewCallback.onGuideViewClick(); } }
if(baseGuideView == null){ baseGuideView = new NanShiGuide(getApplicationContext(), new GuideViewCallback() { @Override public void onGuideViewClick() { // TODO Auto-generated method stub hideGuideView(); } }); guideLayout.addView(baseGuideView); }
方法二:不通過LayoutInflater來映射,而是直接使用類名映射
請參考我的前文:http://blog.csdn.net/yanzi1225627/article/details/30763555 的HeadControlPanel.java的封裝方法。這種方法不適合做動態添加,因為它不能new,只能通過在母布局裡include來添加。正因為它是從布局裡加載的,因此會調用onFinishInflate()流程,當執行到此時表示布局已經加載進來了,裡面的孩子view可以實例化了。 但第一種方法是不會調用onFinishInflate的,所以必須用LayoutInflator。 再者,使用第二種方法也就意味著自定義view的構造函數只能是:
public NanShiGuide(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
無法再多傳遞其他重要變量。
綜合兩種方法的優缺點,我個人強烈建議使用第一種方式來自定義ViewGroup,但google的部分原生應用裡使用的是第二種方法。本文代碼使用第一種方式。另外,這兩種加載機制不同,所以在對view動態改變位置時也會不同。
方法一:設置LayoutParams,通過params設置四個margin來改變
方法二:通過setX()、setY()這兩個函數直接設置坐標位置。
方法三:通過setTranslationX、setTranslationY來設置相對偏移量,當然是在onLayout流程裡。
這三種方法裡個人最推薦的是第三種,除此外方法1在有些場合下也會用到,方法2比較坑爹一般不用。下面是方法3的示例,先來看一副圖片:
自然狀態下,圖片靠左上頂點擺放:
下圖為旋轉了90°後,我在代碼裡guideNanLayout.setRotation()進行旋轉後的。guideNanLayout就是那個圖片的布局。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD48cD48aW1nIHNyYz0="/uploadfile/Collfiles/20140728/20140728091329142.png" alt="\" />
記View的寬度為W,高度為H。如上圖所示,在旋轉90°後,圖片在x軸和y軸上分別塌縮了Abs(W - H) / 2的像素。為此,我們可以首先把這個“塌縮”給補回來,讓旋轉90°後的view還是以左上頂點為基准點,之後用如下代碼進行平移。
guideNanLayout.setTranslationX(transX);
guideNanLayout.setTranslationY(transY);
最終的onLayout函數如下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub Log.i(TAG, "onLayout enter..."); Log.i(TAG, "width = " + getWidth() + " height = " + getHeight()); int transX = 0; int transY = 0; if(mOrientation == 0){ guideNanLayout.setRotation(0); transX += 0; transY += 0; }else if(mOrientation == 270){ guideNanLayout.setRotation(90); transX += -DisplayUtil.dip2px(mContext, 25) + DisplayUtil.dip2px(mContext, 210); transY += DisplayUtil.dip2px(mContext, 25); }else if(mOrientation == 180){ guideNanLayout.setRotation(180); transX += DisplayUtil.dip2px(mContext, 160); transY += b - DisplayUtil.dip2px(mContext, 150); }else if(mOrientation == 90){ guideNanLayout.setRotation(270); transX += -DisplayUtil.dip2px(mContext, 25); transY += b - DisplayUtil.dip2px(mContext, 200 - 25); } guideNanLayout.setTranslationX(transX); guideNanLayout.setTranslationY(transY); // this.setTranslationX(transX); // this.setTranslationY(transY); // RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) guideNanLayout.getLayoutParams(); // params.leftMargin = 100; // params.topMargin = 100; // guideNanLayout.setLayoutParams(params); super.onLayout(changed, l, t, r, b); Log.i(TAG, "onLayout exit..."); }最終旋轉屏幕時效果圖如下:
注意這塊我並沒用android自有的讓布局旋轉的那種機制,那個效果不好,轉換太慢。因為onLayout裡設置偏移量是在onDraw前,所以此方法方向變換時不會有殘留。即便一開始就90°拿手機,不會出現那種先是正常顯示再轉過去的現象。每次方向變時就設置下角度,然後調用requestLayout():
@Override
public void setOrientation(int orientation, boolean animation) {
// TODO Auto-generated method stub
mOrientation = orientation;
requestLayout();
}
可以參考這裡,當調用requestLayout時會讓View重新measure、layout。
為什麼不用setX()這種方法呢?查看其api解釋:
/** * Sets the visual x position of this view, in pixels. This is equivalent to setting the * {@link #setTranslationX(float) translationX} property to be the difference between * the x value passed in and the current {@link #getLeft() left} property. * * @param x The visual x position of this view, in pixels. */ public void setX(float x) { setTranslationX(x - mLeft); }
// guideNanLayout.setTranslationX(transX);
// guideNanLayout.setTranslationY(transY);
換成:
guideNanLayout.setX(transX);
guideNanLayout.setY(transY);
得到的結果是一模一樣的,這是因為這裡的mLeft等於0的原因。
再來看方法1,通過設置LayoutParams來動態改變位置,這有時好用,但有時完全沒有效果。因為要改變LayoutParams首先view要加載進來,才能get得到。2,這種設params的方法一旦rotate後本身的margins就變了,很難計算旋轉後的margins。
而且更嚴重的是,在本例中在onLayout裡通過
// RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) guideNanLayout.getLayoutParams();
// params.leftMargin = 100;
// params.topMargin = 100;
// guideNanLayout.setLayoutParams(params);
是看不到一點效果的,這是個十分詭異的事情。但將其放在initView或onMeasure裡則是ok的。根據這個現象我認為,在onlayout的時候再對子view設置margins已經晚了,不起作用了,要設margins也必須在onlayout進來之前就設好。
另外有個問題,在onlayout裡默認的setX這些都是this.setX()對應的是母布局的設置,如果對裡面的孩子設置前面必須加上孩子的名字。還有,在super.onLayout(changed, l, t, r, b);之前設置好setTranslationX就好了,並不需要再super.onLayout(changed, l, t, r, b);對這裡的五個參數進行改變。
其實看setLayoutParams(params)的流程可以知道:
public void setLayoutParams(ViewGroup.LayoutParams params) { if (params == null) { throw new NullPointerException("Layout parameters cannot be null"); } mLayoutParams = params; resolveLayoutParams(); if (mParent instanceof ViewGroup) { ((ViewGroup) mParent).onSetLayoutParams(this, params); } requestLayout(); }
至此旋轉搞好了,接下來是如何獲得角度:
mOrientationEvent= new OrientationEventListener(this) { @Override public void onOrientationChanged(int orientation) { // TODO Auto-generated method stub if(orientation == OrientationEventListener.ORIENTATION_UNKNOWN){ return; } mOrientation = RoundUtil.roundOrientation(orientation, mOrientation); int orientationCompensation = (mOrientation + RoundUtil .getDisplayRotation(MainActivity.this)) % 360; if(mOrientationCompensation != orientationCompensation){ mOrientationCompensation = orientationCompensation; Log.i("YanZi", "mOrientationCompensation = " + mOrientationCompensation); OrientationUtil.setOrientation(mOrientationCompensation == -1 ? 0 : mOrientationCompensation); setOrientation(OrientationUtil.getOrientation(), false); } }
@Override protected void onResume() { // TODO Auto-generated method stub super.onResume(); mOrientationEvent.enable(); } @Override protected void onPause() { // TODO Auto-generated method stub super.onPause(); mOrientationEvent.disable(); }用到的RoundUtil:
package org.yanzi.util; import android.app.Activity; import android.view.OrientationEventListener; import android.view.Surface; public class RoundUtil { public static final int ORIENTATION_HYSTERESIS = 5; public static int roundOrientation(int orientation, int orientationHistory) { boolean changeOrientation = false; if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) { changeOrientation = true; } else { int dist = Math.abs(orientation - orientationHistory); dist = Math.min( dist, 360 - dist ); changeOrientation = ( dist >= 45 + ORIENTATION_HYSTERESIS ); } if (changeOrientation) { return ((orientation + 45) / 90 * 90) % 360; } return orientationHistory; } public static int getDisplayRotation(Activity activity) { int rotation = activity.getWindowManager().getDefaultDisplay() .getRotation(); switch (rotation) { case Surface.ROTATION_0: return 0; case Surface.ROTATION_90: return 90; case Surface.ROTATION_180: return 180; case Surface.ROTATION_270: return 270; } return 0; } }
最後,一個view通過rotate()無論怎麼轉都是以自身的中心點進行旋轉的,只要母布局麼有旋轉,坐標系原點就是屏幕左上角,且x、y軸不交換。
源碼下載:http://download.csdn.net/detail/yanzi1225627/7681731
--------------------本文系原創,轉載請注明作者yanzi1225627
XML初步今天我們來學習另一種非常重要的數據交換格式-XML。XML(Extensible Markup Language的縮寫,意為可擴展的標記語言),它是一種元標記
1 . 以下集合對象中哪幾個是線程安全的?(B,C,D )A: ArrayListB: VectorC: HashtableD: Stack解析:下面是這些線程安全的同步
按照大神的思路寫出了一個流式布局,所有的東西都是難者不會會者不難,當自己能自定義流式布局的時候就會覺得這東西原來很簡單了。如果各位小伙伴也看過那篇文章的話,應該知道自定義
由於隨手拍項目想做成類似於美圖秀秀那種底部有一排Menu實現不同效果的功能,這裡先簡單介紹如何通過Menu實現打開相冊中的圖片、懷舊效果、浮雕效果、光照效果和素描效果.後