編輯:關於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
Android中XML數據解析
XML初步今天我們來學習另一種非常重要的數據交換格式-XML。XML(Extensible Markup Language的縮寫,意為可擴展的標記語言),它是一種元標記
Java重難點面試(一)
1 . 以下集合對象中哪幾個是線程安全的?(B,C,D )A: ArrayListB: VectorC: HashtableD: Stack解析:下面是這些線程安全的同步
Android開發之玩轉FlexboxLayout布局
按照大神的思路寫出了一個流式布局,所有的東西都是難者不會會者不難,當自己能自定義流式布局的時候就會覺得這東西原來很簡單了。如果各位小伙伴也看過那篇文章的話,應該知道自定義
[Android] 通過Menu實現圖片懷舊、浮雕、模糊、光照和素描效果
由於隨手拍項目想做成類似於美圖秀秀那種底部有一排Menu實現不同效果的功能,這裡先簡單介紹如何通過Menu實現打開相冊中的圖片、懷舊效果、浮雕效果、光照效果和素描效果.後