允许Android随着屏幕转动的控制自由转移到任何地方(附demo)
在本文中,Android ViewGroup/View流程,及經常使用的自己定義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里當然也能正常得到。
? ? 以下是我截的一段代碼:
@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...
能夠看到。在第一次進到onMeasure里運行完super.onMeasure(widthMeasureSpec, heightMeasureSpec);后就能夠得到MeasureWidth和MeasureHeight了。
至于為啥要進兩次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?都大同小異。能夠看看。
二、常見的兩種自己定義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>
能夠看到布局里并沒出現不論什么自己定義信息。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();}}
這是一種最經常使用的方法,核心是initView里通過LayoutInflater.from(mContext).inflate(LAYOUT_ID, this, true);完畢布局xml文件的映射。
LayoutInflater使用參見這里。這樣的寫法最大的優點是即能夠用java語句new一個view add到母布局里。也能夠通過<org.yanzi.ui.NanShiGuide>在xml里使用。個人比較推薦此寫法。
動態加入演示樣例:
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的封裝方法。這樣的方法不適合做動態加入,由于它不能new,僅僅能通過在母布局里include來加入。正由于它是從布局里載入的,因此會調用onFinishInflate()流程,當運行到此時表示布局已經載入進來了,里面的孩子view能夠實例化了。 但第一種方法是不會調用onFinishInflate的,所以必須用LayoutInflator。
再者。使用另外一種方法也就意味著自己定義view的構造函數僅僅能是:
public NanShiGuide(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
無法再多傳遞其它重要變量。
? ? 綜合兩種方法的優缺點,我個人強烈建議使用第一種方式來自己定義ViewGroup。但google的部分原生應用里使用的是另外一種方法。
本文代碼使用第一種方式。另外,這兩種載入機制不同,所以在對view動態改變位置時也會不同。
三、三種動態改變View位置的方法
? ? 方法一:設置LayoutParams,通過params設置四個margin來改變
? ? 方法二:通過setX()、setY()這兩個函數直接設置坐標位置。
? ? 方法三:通過setTranslationX、setTranslationY來設置相對偏移量。當然是在onLayout流程里。
這三種方法里個人最推薦的是第三種,除此外方法1在有些場合下也會用到,方法2比較坑爹一般不用。
以下是方法3的演示樣例。先來看一副圖片:
自然狀態下,圖片靠左上頂點擺放:
下圖為旋轉了90°后,我在代碼里guideNanLayout.setRotation()進行旋轉后的。guideNanLayout就是那個圖片的布局。
記View的寬度為W,高度為H。如上圖所看到的,在旋轉90°后,圖片在x軸和y軸上分別塌縮了Abs(W - H) / 2的像素。
為此,我們能夠首先把這個“塌縮”給補回來。讓旋轉90°后的view還是以左上頂點為基準點。之后用例如以下代碼進行平移。
guideNanLayout.setTranslationX(transX);
guideNanLayout.setTranslationY(transY);
終于的onLayout函數例如以下:
@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...");}終于旋轉屏幕時效果圖例如以下:
注意這塊我并沒用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);}
事實上setX終于還是調用的setTranslationX。因此不如直接調用setTranslationX。
在本文的演示樣例代碼中將:
// 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();}
設完參數后終于調的是requestLayout(),即請求對自身又一次measure和layout.從這個角度講,通過params來改變位置比較低效。還須要再走一遍自己的流程。而在母布局里的onLayout里setTranslateX則不額外添加流程。至于為啥在onLayout里設置子view的params無效。這個著實無從查起,個人推測是母布局onLayout的時候不額外獲取子view的其它參數。僅僅從xml里讀的。但是在上面介紹自己定義VIewGroup的時候,里面的方法2是能夠在onlayout里通過設置margin來動態布局子view的。
參見我的前文:Android應用經典主界面框架之中的一個:仿QQ (使用Fragment, 附源代碼)里的layoutItems()函數。
? ? 至此旋轉搞好了。接下來是怎樣獲得角度:
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,比我之前的博文要嚴謹。
? ? 最后,一個view通過rotate()不管怎么轉都是以自身的中心點進行旋轉的,僅僅要母布局么有旋轉,坐標系原點就是屏幕左上角。且x、y軸不交換。
源代碼下載:http://download.csdn.net/detail/yanzi1225627/7681731
--------------------本文系原創,轉載請注明作者yanzi1225627
版權聲明:本文博主原創文章,博客,未經同意不得轉載。
轉載于:https://www.cnblogs.com/zfyouxi/p/4797719.html
總結
以上是生活随笔為你收集整理的允许Android随着屏幕转动的控制自由转移到任何地方(附demo)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为啥泰山的土壤类型如此多样?
- 下一篇: 为啥泰山的地貌如此复杂多样?