允许Android随着屏幕转动的控制自由转移到任何地方(附demo)
在本文中,Android ViewGroup/View流程,及經(jīng)常使用的自己定義ViewGroup的方法。在此基礎(chǔ)上介紹動態(tài)控制View的位置的三種方法,并給出最佳的一種方法。
一、ViewGroup/View的繪制流程
簡單的說一個View從無到有須要三個步驟,onMeasure、onLayout、onDraw,即測量大小、放置位置、繪制三個步驟。
而ViewGroup的onMeasure、onLayout流程里,又會遍歷每一個孩子。并終于調(diào)到孩子的measure()、layout()函數(shù)里。
與View不同的是。ViewGroup沒有onDraw流程,但有dispatchDraw()流程,該函數(shù)終于又調(diào)用drawChild()繪制每一個孩子,調(diào)每一個孩子View的onDraw流程。
在onMeasure流程里是為了獲得控件的高和寬,這塊有個getWidth()和getMeasuredWidth()的概念,前者指寬度,后者是測量寬度。一般來說。一個自己定義VIewGroup(如繼承自RelativeLayout)一般要進(jìn)兩次onMeasure,一次onLayout,一次drawChild()。盡管onMeasure流程是測量大小。且進(jìn)了兩次。但直到最后一次出去的時候調(diào)用getWidth()得到的仍然是0.getWidth()的數(shù)值一直到onSizeChanged()的時候才干夠得到正確的,此后進(jìn)到onLayout里當(dāng)然也能正常得到。
? ? 以下是我截的一段代碼:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// TODO Auto-generated method stubLog.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 = 0Line 359: 01-03 10:15:40.527 I/YanZi (10793): MeasuredWidth = 0 MeasuredHeight = 0Line 361: 01-03 10:15:40.531 I/YanZi (10793): 00000000000 width = 0 height = 0Line 363: 01-03 10:15:40.532 I/YanZi (10793): 00000000000 MeasuredWidth = 1080 MeasuredHeight = 1701Line 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 = 0Line 371: 01-03 10:15:40.533 I/YanZi (10793): MeasuredWidth = 1080 MeasuredHeight = 1701Line 373: 01-03 10:15:40.536 I/YanZi (10793): 00000000000 width = 0 height = 0Line 375: 01-03 10:15:40.536 I/YanZi (10793): 00000000000 MeasuredWidth = 1080 MeasuredHeight = 1701Line 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 = 1701Line 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 = 1701Line 389: 01-03 10:15:40.540 I/YanZi (10793): onLayout exit...
能夠看到。在第一次進(jìn)到onMeasure里運(yùn)行完super.onMeasure(widthMeasureSpec, heightMeasureSpec);后就能夠得到MeasureWidth和MeasureHeight了。
至于為啥要進(jìn)兩次onMeasure,翻遍了網(wǎng)絡(luò)么有找到合理的解釋。有人說是大小發(fā)生變化時要進(jìn)兩次,如Linearlayout里設(shè)置了weight屬性,則第一次測量時得到一個大小,第二次測量時把weight加上得到終于的大小。
但是我用Linearlayout把里面全部的母和子的view大小都寫死,onMeasure還是進(jìn)了兩次。
RelativeLayout就不用說了也是進(jìn)的兩次。國外文檔也有解釋說,當(dāng)子view不能夠填滿父控件時。要第二次進(jìn)到onMeasure里。
經(jīng)我測試。貌似也是扯淡。我全都match_parent還是進(jìn)了兩次。
? ? 當(dāng)然在onMeasure里能夠直接setMeasuredDimension(measuredWidth, measuredHeight)設(shè)置控件寬和高,這樣不管xml里咋寫的,終于以此句設(shè)置的width和height進(jìn)行放置、顯示。
關(guān)于View/ViewGroup繪制原理本文就介紹到這。更具體請參考:鏈接1 鏈接2 鏈接3 鏈接4?都大同小異。能夠看看。
二、常見的兩種自己定義ViewGroup的方法
方法一:
c_nanshi_guide.xml布局文件
<?
xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <FrameLayout android:id="@+id/guide_nan_layout" android:layout_width="200dp" android:layout_height="150dp" android:background="@drawable/nan1" > <TextView android:id="@+id/guide_nan_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:gravity="center" android:text="南公懷瑾." android:textColor="@android:color/white" android:textSize="20sp" /> </FrameLayout> </RelativeLayout>
能夠看到布局里并沒出現(xiàn)不論什么自己定義信息。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 stubmContext = context;initView();mDrawable = context.getResources().getDrawable(R.drawable.ong);}@Overrideprotected void initView() {// TODO Auto-generated method stubLog.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);}@Overrideprotected void onFinishInflate() {// TODO Auto-generated method stubLog.i(TAG, "onFinishInflate enter...");super.onFinishInflate();}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {// TODO Auto-generated method stubLog.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...");}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// TODO Auto-generated method stubLog.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...");}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {// TODO Auto-generated method stubLog.i(TAG, "onSizeChanged enter...");Log.i(TAG, "width = " + getWidth() + " height = " + getHeight());super.onSizeChanged(w, h, oldw, oldh);Log.i(TAG, "onSizeChanged exit...");}@Overrideprotected void onDraw(Canvas canvas) {// TODO Auto-generated method stubLog.i(TAG, "onDraw enter...");super.onDraw(canvas);}@Overrideprotected void dispatchDraw(Canvas canvas) {// TODO Auto-generated method stubLog.i(TAG, "dispatchDraw enter...");super.dispatchDraw(canvas);}@Overrideprotected boolean drawChild(Canvas canvas, View child, long drawingTime) {// TODO Auto-generated method stubLog.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);}}
BaseGuideView.java例如以下:
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 stubmContext = context;mGuideViewCallback = callback;setOnClickListener(this);mOrientation = OrientationUtil.getOrientation();}@Overridepublic void setOrientation(int orientation, boolean animation) {// TODO Auto-generated method stubmOrientation = orientation;requestLayout();}protected abstract void initView();@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {// TODO Auto-generated method stubreturn true; //super.onInterceptTouchEvent(ev)}@Overridepublic void onClick(View v) {// TODO Auto-generated method stubmGuideViewCallback.onGuideViewClick();}}
這是一種最經(jīng)常使用的方法,核心是initView里通過LayoutInflater.from(mContext).inflate(LAYOUT_ID, this, true);完畢布局xml文件的映射。
LayoutInflater使用參見這里。這樣的寫法最大的優(yōu)點(diǎn)是即能夠用java語句new一個view add到母布局里。也能夠通過<org.yanzi.ui.NanShiGuide>在xml里使用。個人比較推薦此寫法。
動態(tài)加入演示樣例:
if(baseGuideView == null){baseGuideView = new NanShiGuide(getApplicationContext(), new GuideViewCallback() {@Overridepublic void onGuideViewClick() {// TODO Auto-generated method stubhideGuideView();}});guideLayout.addView(baseGuideView);}
方法二:不通過LayoutInflater來映射,而是直接使用類名映射
請參考我的前文:http://blog.csdn.net/yanzi1225627/article/details/30763555?的HeadControlPanel.java的封裝方法。這樣的方法不適合做動態(tài)加入,由于它不能new,僅僅能通過在母布局里include來加入。正由于它是從布局里載入的,因此會調(diào)用onFinishInflate()流程,當(dāng)運(yùn)行到此時表示布局已經(jīng)載入進(jìn)來了,里面的孩子view能夠?qū)嵗恕?但第一種方法是不會調(diào)用onFinishInflate的,所以必須用LayoutInflator。
再者。使用另外一種方法也就意味著自己定義view的構(gòu)造函數(shù)僅僅能是:
public NanShiGuide(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
無法再多傳遞其它重要變量。
? ? 綜合兩種方法的優(yōu)缺點(diǎn),我個人強(qiáng)烈建議使用第一種方式來自己定義ViewGroup。但google的部分原生應(yīng)用里使用的是另外一種方法。
本文代碼使用第一種方式。另外,這兩種載入機(jī)制不同,所以在對view動態(tài)改變位置時也會不同。
三、三種動態(tài)改變View位置的方法
? ? 方法一:設(shè)置LayoutParams,通過params設(shè)置四個margin來改變
? ? 方法二:通過setX()、setY()這兩個函數(shù)直接設(shè)置坐標(biāo)位置。
? ? 方法三:通過setTranslationX、setTranslationY來設(shè)置相對偏移量。當(dāng)然是在onLayout流程里。
這三種方法里個人最推薦的是第三種,除此外方法1在有些場合下也會用到,方法2比較坑爹一般不用。
以下是方法3的演示樣例。先來看一副圖片:
自然狀態(tài)下,圖片靠左上頂點(diǎn)擺放:
下圖為旋轉(zhuǎn)了90°后,我在代碼里guideNanLayout.setRotation()進(jìn)行旋轉(zhuǎn)后的。guideNanLayout就是那個圖片的布局。
記View的寬度為W,高度為H。如上圖所看到的,在旋轉(zhuǎn)90°后,圖片在x軸和y軸上分別塌縮了Abs(W - H) / 2的像素。
為此,我們能夠首先把這個“塌縮”給補(bǔ)回來。讓旋轉(zhuǎn)90°后的view還是以左上頂點(diǎn)為基準(zhǔn)點(diǎn)。之后用例如以下代碼進(jìn)行平移。
guideNanLayout.setTranslationX(transX);
guideNanLayout.setTranslationY(transY);
終于的onLayout函數(shù)例如以下:
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {// TODO Auto-generated method stubLog.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...");}終于旋轉(zhuǎn)屏幕時效果圖例如以下:
注意這塊我并沒用android自有的讓布局旋轉(zhuǎn)的那種機(jī)制,那個效果不好,轉(zhuǎn)換太慢。
由于onLayout里設(shè)置偏移量是在onDraw前,所以此方法方向變換時不會有殘留。即便一開始就90°拿手機(jī),不會出現(xiàn)那種先是正常顯示再轉(zhuǎn)過去的現(xiàn)象。每次方向變時就設(shè)置下角度,然后調(diào)用requestLayout():
@Override
public void setOrientation(int orientation, boolean animation) {
// TODO Auto-generated method stub
mOrientation = orientation;
requestLayout();
}
能夠參考這里。當(dāng)調(diào)用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);}
事實(shí)上setX終于還是調(diào)用的setTranslationX。因此不如直接調(diào)用setTranslationX。
在本文的演示樣例代碼中將:
// guideNanLayout.setTranslationX(transX);
// guideNanLayout.setTranslationY(transY);
換成:
guideNanLayout.setX(transX);
guideNanLayout.setY(transY);
得到的結(jié)果是一模一樣的,這是由于這里的mLeft等于0的原因。
? ? 再來看方法1。通過設(shè)置LayoutParams來動態(tài)改變位置,這有時好用。但有時全然沒有效果。由于要改變LayoutParams首先view要載入進(jìn)來,才干get得到。
2。這樣的設(shè)params的方法一旦rotate后本身的margins就變了,非常難計(jì)算旋轉(zhuǎn)后的margins。
? ? 并且更嚴(yán)重的是,在本例中在onLayout里通過
// RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) guideNanLayout.getLayoutParams();
// params.leftMargin = 100;
// params.topMargin = 100;
// guideNanLayout.setLayoutParams(params);
是看不到一點(diǎn)效果的。這是個十分詭異的事情。
但將其放在initView或onMeasure里則是ok的。
依據(jù)這個現(xiàn)象我覺得,在onlayout的時候再對子view設(shè)置margins已經(jīng)晚了,不起作用了。要設(shè)margins也必須在onlayout進(jìn)來之前就設(shè)好。
? ? 另外有個問題,在onlayout里默認(rèn)的setX這些都是this.setX()相應(yīng)的是母布局的設(shè)置,假設(shè)對里面的孩子設(shè)置前面必須加上孩子的名字。還有。在super.onLayout(changed, l, t, r, b);之前設(shè)置好setTranslationX就好了。并不須要再super.onLayout(changed, l, t, r, b);對這里的五個參數(shù)進(jìn)行改變。
事實(shí)上看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();}
設(shè)完參數(shù)后終于調(diào)的是requestLayout(),即請求對自身又一次measure和layout.從這個角度講,通過params來改變位置比較低效。還須要再走一遍自己的流程。而在母布局里的onLayout里setTranslateX則不額外添加流程。至于為啥在onLayout里設(shè)置子view的params無效。這個著實(shí)無從查起,個人推測是母布局onLayout的時候不額外獲取子view的其它參數(shù)。僅僅從xml里讀的。但是在上面介紹自己定義VIewGroup的時候,里面的方法2是能夠在onlayout里通過設(shè)置margin來動態(tài)布局子view的。
參見我的前文:Android應(yīng)用經(jīng)典主界面框架之中的一個:仿QQ (使用Fragment, 附源代碼)里的layoutItems()函數(shù)。
? ? 至此旋轉(zhuǎn)搞好了。接下來是怎樣獲得角度:
mOrientationEvent= new OrientationEventListener(this) {@Overridepublic void onOrientationChanged(int orientation) {// TODO Auto-generated method stubif(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);}}
@Overrideprotected void onResume() {// TODO Auto-generated method stubsuper.onResume();mOrientationEvent.enable();}@Overrideprotected void onPause() {// TODO Auto-generated method stubsuper.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,比我之前的博文要嚴(yán)謹(jǐn)。
? ? 最后,一個view通過rotate()不管怎么轉(zhuǎn)都是以自身的中心點(diǎn)進(jìn)行旋轉(zhuǎn)的,僅僅要母布局么有旋轉(zhuǎn),坐標(biāo)系原點(diǎn)就是屏幕左上角。且x、y軸不交換。
源代碼下載:http://download.csdn.net/detail/yanzi1225627/7681731
--------------------本文系原創(chuàng),轉(zhuǎn)載請注明作者yanzi1225627
版權(quán)聲明:本文博主原創(chuàng)文章,博客,未經(jīng)同意不得轉(zhuǎn)載。
轉(zhuǎn)載于:https://www.cnblogs.com/zfyouxi/p/4797719.html
總結(jié)
以上是生活随笔為你收集整理的允许Android随着屏幕转动的控制自由转移到任何地方(附demo)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为啥泰山的土壤类型如此多样?
- 下一篇: Android Cursor类的概念和用