Android鏡像翻轉指的是將屏幕進行水平的翻轉,達到所有內容顯示都會反向的效果,就像是在鏡子中看到的界面一樣。這種應用的使用場景相對比較受限,主要用在一些需要使用Android手機界面進行鏡面投影的地方,比如說車載手機hud導航之類的。
鏡像翻轉的效果如下:

鏡像水平翻轉前后效果
在沒辦法對硬件進行直接翻轉的時候,只能從代碼進行着手。最先想到的方法是一種比較弱的實現方案,就是對界面進行截圖,然后對截圖進行翻轉,再讓其替換掉原先的界面,這種方法是可行的,但是會出現很嚴重的內存問題,因為圖片很耗內存,而且不利於動態界面的實現,比如控件會變動位置,控件內容會變化的情況。這就需要其他更靠譜的方案。
下面提供三種解決方案,能夠解決一部分鏡像翻轉問題。
1.翻轉動畫
第一種方法是使用Android翻轉動畫進行實現。
該方法需要重寫動畫,實現翻轉,並將該動畫添加到布局中,之后只要將動畫的時長設置到0就能忽略掉動畫過程,從而直接獲取到動畫的最終效果。需要重寫Animate類,用 android.graphics.Camera和 android.graphics.Matrix可以比較容易地實現翻轉效果,代碼實現如下:
-
1 /** 2 * Created by obo on 15/11/26. 3 */ 4 5 import android.graphics.Camera; 6 import android.graphics.Matrix; 7 import android.view.animation.Animation; 8 import android.view.animation.Transformation; 9 10 public class Rotate3dAnimation extends Animation { 11 12 // 中心點 13 private final float mCenterX; 14 private final float mCenterY; 15 // 3D變換處理camera(不是攝像頭) 16 private Camera mCamera = new Camera(); 17 18 /** 19 * @param centerX 翻轉中心x坐標 20 * @param centerY 翻轉中心y坐標 21 */ 22 public Rotate3dAnimation(float centerX, 23 float centerY) { 24 mCenterX = centerX; 25 mCenterY = centerY; 26 } 27 28 @Override 29 protected void applyTransformation(float interpolatedTime, Transformation t) { 30 // 生成中間角度 31 final Camera camera = mCamera; 32 final Matrix matrix = t.getMatrix(); 33 camera.save(); 34 camera.rotateY(180); 35 // 取得變換后的矩陣 36 camera.getMatrix(matrix); 37 38 camera.restore(); 39 matrix.preTranslate(-mCenterX, -mCenterY); 40 matrix.postTranslate(mCenterX, mCenterY); 41 } 42 }
調用的方法如下:
View layoutView = findViewById(R.id.reverse_layout); Animation animation = new Rotate3dAnimation(layoutView.getWidth() / 2, layoutView.getHeight() / 2); animation.setFillAfter(true); layoutView.startAnimation(animation);
這里的reverse_layout是一個RelativeLayout的布局,調用了該段代碼之后能將Layout和layout所承載的內容都進行翻轉。思路是將layoutView從中心點進行180度的水平翻轉,需要設置setFillAfter為true來保持翻轉后的最終狀態。這里需要注意的是,這段代碼不能直接放在onCreate里面調用,因為在onCreate的時候,layout的大小還沒有被計算出來,如果想在onCreate里面使用可以這樣:
-
1 final View layoutView = findViewById(R.id.reverse_layout); 2 3 ViewTreeObserver vto = layoutView.getViewTreeObserver(); 4 vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 5 @Override 6 public void onGlobalLayout() { 7 8 Animation animation = new Rotate3dAnimation(layoutView.getMeasuredWidth() / 2, layoutView.getMeasuredHeight() / 2); 9 animation.setFillAfter(true); 10 layoutView.startAnimation(animation); 11 } 12 });
可以為layoutView加一個布局的監聽,監聽到layoutView布局加載了之后,就能正常獲取layoutView的大小了,也就能正常輸出效果了。
2.重寫控件
對控件進行重寫是另外一個實現的思路。假設承載界面的Layout是RelativeLayout,則可以對整個RelativeLayout進行重寫,重寫的代碼可以如下:
1 import android.content.Context; 2 import android.graphics.Canvas; 3 import android.util.AttributeSet; 4 import android.widget.RelativeLayout; 5 6 /** 7 * Created by obo on 15/12/4. 8 */ 9 public class ReverseLayout extends RelativeLayout { 10 11 public boolean isReverse = true; 12 13 public ReverseLayout(Context context, AttributeSet attrs) { 14 super(context, attrs); 15 } 16 17 @Override 18 public void dispatchDraw(Canvas canvas) { 19 20 if (isReverse) 21 canvas.scale(-1, 1, getWidth() / 2, getHeight() / 2); 22 23 super.dispatchDraw(canvas); 24 } 25 }
之后,在布局xml中將最外層的RelativeLayout替換成ReverseLayout就能對界面進行翻轉。這樣的翻轉能夠將Layout里面所有的控件都進行翻轉,如果需要翻轉的僅僅只是一個TextView的話,則可以單單對一個TextView進行重寫,這個時候,就不需要重寫dispatchDraw方法,而應該重寫onDraw方法,如下:
-
import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.widget.TextView; /** * Created by obo on 15/12/6. */ public class ReverseTextView extends TextView { public ReverseTextView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public void onDraw(Canvas canvas) { canvas.scale(-1, 1, getWidth() / 2, getHeight() / 2); super.onDraw(canvas); } }
onDraw和dispatchDraw的區別是onDraw只對當前的View有效,而不會影響其所包含的SubView,而dispatchDraw則會將翻轉效果傳遞到所有的SubView。
3.SurfaceView翻轉
以上兩種方法能實現大多數View的翻轉,但是都對SurfaceView沒有效果,因為SurfaceView是通過雙緩沖機制進行繪制的,不會經過onDraw或是dispatchDraw方法,也就不能對我們所進行的操作進行響應了,對於自定義的SurfaceView來說,可以對在lockCanvas中獲取的Canvas對象進行翻轉處理。
下面給出SurfaceView翻轉實現的代碼:
-
import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.SurfaceHolder; import android.view.SurfaceView; /** * Created by obo on 15/12/6. */ public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback{ SurfaceHolder surfaceHolder ; public TestSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); surfaceHolder = this.getHolder(); surfaceHolder.addCallback(this); } @Override public void surfaceCreated(SurfaceHolder holder) { Canvas canvas = surfaceHolder.lockCanvas(); //繪制之前先對畫布進行翻轉 canvas.scale(-1,1, getWidth()/2,getHeight()/2); //開始自己的內容的繪制 Paint paint = new Paint(); canvas.drawColor(Color.WHITE); paint.setColor(Color.BLACK); paint.setTextSize(50); canvas.drawText("這是對SurfaceView的翻轉",50,250,paint); surfaceHolder.unlockCanvasAndPost(canvas); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceHolder holder) {} }
采用該方法之后,對SurfaceView也能進行翻轉了,效果如下:

實際上,該方法是借助了第二種方法的思路,直接對canvas進行預先的處理從而達到我們所需要的效果的,所以也可以作為第二種方法的擴展。
4.手勢翻轉
需要注意的是,以上這幾種方法僅僅是實現了顯示的翻轉,手勢操作的位置並沒有發生翻轉。所以使用以上翻轉方式的話需要結合手勢翻轉的實現,其實現思路是重寫外層的viewgroup的onInterceptTouchEvent方法,對下發的MotionEvent進行一次翻轉操作,使得childView接收到的手勢都是反過來的。實現代碼如下 :
5.更優雅的方案
對於普通view(非SurfaceView),還有一個更加優雅的實現方案,而且不需要重寫onInterceptTouchEvent方法,只需要調用父布局的setScaleY或者setScaleX方法即可。
- // 獲取需要翻轉的父布局
- layoutScale = findViewById(R.id.layout_scale);
- // 翻轉
- layoutScale.setScaleY(-1);
6.總結
package com.obo.reverseview.views.touchreverse; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.widget.RelativeLayout; /** * Created by obo on 16/5/4. * Email:obo1993@gmail.com * Git:https://github.com/OboBear * Blog:http://blog.csdn.net/leilba */ public class TouchReverseLayout extends RelativeLayout { public TouchReverseLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ev.setLocation(this.getWidth() - ev.getX(),ev.getY()); return super.onInterceptTouchEvent(ev); } }
采用動畫和重寫控件的方案都能實現界面翻轉的效果,而且性能方面都十分不錯。
但是這兩種方法都會存在以下一些問題:
1.僅僅翻轉顯示內容,不會翻轉點擊的坐標位置。也就是說,如果布局內最左邊存在着一個按鈕,則翻轉后,按鈕將會顯示在界面最右邊,但是想要點擊按鈕的話,還是在界面原先按鈕所在的最左邊進行點擊才會得到響應。這里可以采取在父布局中對坐標進行重新定位的方法。
2. 無法翻轉已經封裝好了的SurfaceView。比如說,當前要將某第三方地圖界面進行水平鏡像翻轉,發現用第一種和第二種方法都無效,查看了部分源碼之后發現其實質是用SurfaceView進行實現的,但是SurfaceView是作為該地圖的一個subView存在的,所以不能直接獲取到該subView,也不能到該SubView的繪制層獲取canvas了,這個時候第三種方法也無法進行施展,這里需要采用動態代理的方式來解決,可見Android動態代理為SurfaceHolder添加Hook
代碼例子發布在github:
