12.建立檢視變換
12.1 問題
應用程式需要動態變換檢視的外觀,從而為檢視新增一些視覺效果,例如視覺變換效果。
12.2 解決方案
(API Level 1)
Viewgroup中的靜態變換API提供了應用視覺效果的簡單方法,例如旋轉、縮放、透明度變化,而且不必依靠動畫。使用它也很容易使用父檢視的上下文來應用變換,例如根據位置的變化而縮放。
在初始化過程中呼叫setStaticTranformationsEnabled(true),可以啟用任何ViewGroup的靜態變換。啟動此功能後,框架會定期呼叫每個檢視的getChildStaticTransformation(),從而允許應用程式設定變換。
12.3 實現機制
首先看一個示例,在該例中變換被應用一次而且不會改變(參見以下程式碼)。
使用靜態變換自定義佈局
public class PerspectiveLayout extends LinearLayout { public PerspectiveLayout(Context context) { super(context); init(); } public PerspectiveLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public PerspectiveLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { // 啟動靜態變換,這樣對每個檢視都會呼叫getChildStaticTransformation() setStaticTransformationsEnabled(true); } @Override protected boolean getChildStaticTransformation(View child, Transformation t) { // 清除所有現有的變換 t.clear(); if (getOrientation() == HORIZONTAL) { // 根據到左邊緣的距離對子檢視進行縮放 float delta = 1.0f - ((float) child.getLeft() / getWidth()); t.getMatrix().setScale(delta, delta, child.getWidth() / 2, child.getHeight() / 2); } else { // 根據頂端邊緣的距離對子檢視進行縮放 float delta = 1.0f - ((float) child.getTop() / getHeight()); t.getMatrix().setScale(delta, delta, child.getWidth() / 2, child.getHeight() / 2); //同樣也根據它的位置應用顏色淡出效果 t.setAlpha(delta); } return true; } }
這個示例介紹了一個自定義的LinearLayout,它根據子檢視到父檢視起始邊緣的距離,對每個檢視做了縮放變換。getChildStaticTransformation()中的程式碼通過子檢視到父檢視左邊緣或頂端邊緣的距離與父檢視完整尺寸的比值計算得出應該使用的縮放比例。設定變換之後,這個方法的返回值會通知Android框架。任何情況下,只有應用程式中設定了一個自定義變換,這個方法就必須返回true,以確保它被關聯到檢視上。
大多數的視覺效果(如旋轉或縮放)都實際地應用於Transformation的Matrix上。在我們的示例中,通過呼叫getMatrix().setScale()來調整每個子檢視的縮放,同時傳入縮放比例和軸心點。軸心點就是縮放發生的位置,我們將這個位置設定在檢視的中心點,這樣縮放的結果就會居中顯示。
如果佈局是垂直方向的,我們同樣會根據相同的距離值為子檢視應用透明漸變效果,只需要直接使用Transformation的setAlpha()方法即可。以下程式碼就是使用這個檢視的示例佈局。
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- 水平方向自定義佈局--> <com.examples.statictransforms.PerspectiveLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> </com.examples.statictransforms.PerspectiveLayout> <!-- 垂直方向自定義佈局--> <com.examples.statictransforms.PerspectiveLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> </com.examples.statictransforms.PerspectiveLayout> </LinearLayout>
下圖顯示了示例變換的結果。

水平視角和垂直視角佈局
在水平佈局中,越往右的檢視使用的縮放比例越小。同樣,垂直方向的縮放比例也是越往下越小。另外,垂直方向的檢視由於添加了透明度變化會出現逐漸淡出的效果。
現在讓我們看一下提供了更為動態變化效果的示例。以下程式碼展示了一個封裝在HorizontalScrollView中的自定義佈局。當子檢視滾動時,這個佈局使用靜態變換來縮放子檢視。在螢幕中心的檢視大小總是正常的,越靠近邊緣檢視就越小。這樣就會出現下面的效果:在滾動的過程中,檢視首先會逐漸靠近,然後逐漸遠離。
自定義視角滾動內容public class PerspectiveScrollContentView extends LinearLayout { /* 每個子檢視的縮放比例都是可調節的 */ private static final float SCALE_FACTOR = 0.7f; /* * 變換的軸心點。 (0,0)是左上, (1,1)是右下。當前設定的是底部中間(0.5,1) */ private static final float ANCHOR_X = 0.5f; private static final float ANCHOR_Y = 1.0f; public PerspectiveScrollContentView(Context context) { super(context); init(); } public PerspectiveScrollContentView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public PerspectiveScrollContentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { // 啟動靜態變換,這樣對於每個子檢視,getChildStaticTransformation() // 都會被呼叫 setStaticTransformationsEnabled(true); } /* * 工具方法,用於計算螢幕座標系內所有檢視的當前位置 */ private int getViewCenter(View view) { int[] childCoords = new int[2]; view.getLocationOnScreen(childCoords); int childCenter = childCoords[0] + (view.getWidth() / 2); return childCenter; } @Override protected boolean getChildStaticTransformation(View child, Transformation t) { HorizontalScrollView scrollView = null; if (getParent() instanceof HorizontalScrollView) { scrollView = (HorizontalScrollView) getParent(); } if (scrollView == null) { return false; } int childCenter = getViewCenter(child); int viewCenter = getViewCenter(scrollView); // 計運算元檢視和父容器中心之間的距離,這會決定應用的縮放比例 float delta = Math.min(1.0f, Math.abs(childCenter - viewCenter) / (float) viewCenter); //設定最小縮放比例為0.4 float scale = Math.max(0.4f, 1.0f - (SCALE_FACTOR * delta)); float xTrans = child.getWidth() * ANCHOR_X; float yTrans = child.getHeight() * ANCHOR_Y; //清除現有的所有變換 t.clear(); //為了檢視設定變換 t.getMatrix().setScale(scale, scale, xTrans, yTrans); return true; } }
在這個示例中,自定義佈局會根據每個子檢視相對於父檢視HorizontalScrollView中心位置的距離,為每個子檢視計算變換。當用戶滾動時,每個子檢視的變換需要重新計算,從而實現檢視移動時子檢視的動態放大和縮小。這個示例將變換軸心點設定在每個子檢視的底部中心位置,這會創作出如下效果:每個子檢視會垂直放大,而且保持水平居中。以下程式碼的Activity示例將這個自定義佈局付諸實踐。
使用了PerspectiveScrollContentView的Activity
public class ScrollActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); HorizontalScrollView parentView = new HorizontalScrollView(this); PerspectiveScrollContentView contentView = new PerspectiveScrollContentView(this); // 對此檢視禁用硬體加速,因為動態調整每個子檢視的變換當前無法通過硬體實現 // 也可以通過在清單檔案中新增 android:hardwareAccelerated="false" // 禁用整個Activity或應用程式的硬體加速。但出於效能的考慮,最好儘可能少地 // 禁用硬體加速 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { contentView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); } //向滾動條中新增幾張圖片 for (int i = 0; i < 20; i++) { ImageView iv = new ImageView(this); iv.setImageResource(R.drawable.ic_launcher); contentView.addView(iv); } //新增要顯示的佈局 parentView.addView(contentView); setContentView(parentView); } }
在這個示例中建立一個滾動檢視,並且關聯了一個自定義的包含若干張滾動圖片的PerspectiveScrollContentView。這裡的程式碼不需要太多關注,但有個非常重要的地方需要提一下。雖然一般情況下靜態變換都會被支援,但檢視重新整理過程中動態更新變換效果在當前SDK版本中是不能使用硬體加速的。因此,如果應用程式的目標SDK為11或以上版本,或者已經在某種程度上啟用了硬體加速,這時需要對這個檢視禁用硬體加速。
可以在清單檔案的<activity>或整個<application>標籤中新增android:hardwareAccelerated="false"屬性,對硬體加速進行全域性設定;但是我們也可以通過呼叫setLayerType()方法並設定LAYER_TYPE_SOFTWARE,在Java程式碼中對這個自定義檢視進行單獨設定。如果應用程式的目標SDK版本低於此版本,即使是較新的裝置,預設情況下硬體加速也是關閉的,處於相容性考慮,這些程式碼也許是不必要的。