Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義視圖教程

Android自定義視圖教程

編輯:關於Android編程

  Android的UI元素都是基於View(屏幕中單個元素)和ViewGroup(元素的集合),Android有許多自帶的組件和布局,比如Button、TextView、RelativeLayout。在app開發過程中我們需要自定義視圖組件來滿足我們的需求。通過繼承自View或者View的子類,覆寫onDraw或者onTouchEvent等方法來覆蓋視圖的行為。   創建完全自定義的組件   創建自定義的組件主要圍繞著以下五個方面:   繪圖(Drawing): 控制視圖的渲染,通常通過覆寫onDraw方法來實現 交互(Interaction): 控制用戶和視圖的交互方式,比如OnTouchEvent,gestures 尺寸(Measurement): 控制視圖內容的維度,通過覆寫onMeasure方法 屬性(Attributes): 在XML中定義視圖的屬性,使用TypedArray來獲取屬性值 持久化(Persistence): 配置發生改變時保存和恢復狀態,通過onSaveInstanceState和onRestoreInstanceState 舉個栗子,假設我們想創建一個圖形允許用戶點擊的時候改變形狀(方形、圓形、三角形)。如下所示:       定義視圖類   我們創建一個ShapeSelectorView繼承自View,實現必要的構造器,如下所示:  
public class ShapeSelectorView extends View {
 // We must provide a constructor that takes a Context and an AttributeSet.
 // This constructor allows the UI to create and edit an instance of your view.
 public ShapeSelectorView(Context context, AttributeSet attrs) {
super(context, attrs);
   }
}

 

添加視圖到布局中  
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent" >
   <com.codepath.example.customviewdemo.ShapeSelectorView
android:id="@+id/shapeSelector"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true" />
</RelativeLayout>

 

接下來我們定義一個命名空間app,這個命名空間允許Android自動解析而不需要指定具體的包名。   自定義屬性   視圖可以通過XML來配置屬性和樣式,你需要想清楚要添加那些自定義的屬性,比如我們想讓用戶可以選擇形狀的顏色、是否顯示形狀的名稱,比如我們想讓視圖可以像下面一樣配置:  
<com.codepath.example.customviewdemo.ShapeSelectorView
   app:shapeColor="#7f0000"
   app:displayShapeName="true"
   android:id="@+id/shapeSelector"
   ... />

 

為了能夠定義shapeColor和displayShapeName,我們需要在res/values/attrs.xml中配置:  
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="ShapeSelectorView">
      <attr name="shapeColor" format="color" />
      <attr name="displayShapeName" format="boolean" />
  </declare-styleable>
</resources>

 

對於每個你想自定義的屬性你需要定義attr節點,每個節點有name和format屬性,format屬性是我們期望的值的類型,比如color,dimension,boolean,integer,float等。一旦定義好了屬性,你可以像使用自帶屬性一樣使用他們,唯一的區別在於你的自定義屬性屬於一個不同的命名空間,你可以在根視圖的layout裡面定義命名空間,一般情況下你只需要這樣子指定:http://schemas.android.com/apk/res/<package_name>,但是你可以使用 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent" >
   <com.codepath.example.customviewdemo.ShapeSelectorView
  app:shapeColor="#7f0000"
  app:displayShapeName="true"
  android:id="@+id/shapeSelector"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_above="@+id/btnSelect"
  android:layout_alignParentLeft="true"
  android:layout_below="@+id/tvPrompt" />
</RelativeLayout>

 

應用自定義屬性   在前面我們定義了shapeColor和displayShapeName兩個屬性值,我們需要提取這兩個屬性值來用在自定義的視圖中,可以使用TypedArray和obtainStyledAttributes方法來完成,如下所示:  
public class ShapeSelectorView extends View {
 private int shapeColor;
 private boolean displayShapeName;

 public ShapeSelectorView(Context context, AttributeSet attrs) {
   super(context, attrs);
   setupAttributes(attrs);
 }

 private void setupAttributes(AttributeSet attrs) {
   // Obtain a typed array of attributes
   TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeSelectorView, 0, 0);
   // Extract custom attributes into member variables
   try {
     shapeColor = a.getColor(R.styleable.ShapeSelectorView_shapeColor, Color.BLACK);
     displayShapeName = a.getBoolean(R.styleable.ShapeSelectorView_displayShapeName, false);
   } finally {
     // TypedArray objects are shared and must be recycled.
     a.recycle();
   }
 }
}

 

接下來添加一些getter和setter方法:  
public class ShapeSelectorView extends View {
 // ...
 public boolean isDisplayingShapeName() {
   return displayShapeName;
 }

 public void setDisplayingShapeName(boolean state) {
   this.displayShapeName = state;
   invalidate();
   requestLayout();
 }

 public int getShapeColor() {
   return shapeColor;
 }

 public void setShapeColor(int color) {
   this.shapeColor = color;
   invalidate();
   requestLayout();
 }
}

 

當視圖屬性發生改變的時候可能需要重新繪圖,你需要調用invalidate()和requestLayout()來刷新顯示。   畫圖   假設我們要使用前面的屬性畫一個長方形,所有的繪圖都是在onDraw方法裡執行,使用Canvas對象來繪圖,如下所示:  
public class ShapeSelectorView extends View {
 // ...
 private int shapeWidth = 100;
 private int shapeHeight = 100;
 private int textXOffset = 0;
 private int textYOffset = 30;
 private Paint paintShape;

 // ...
 public ShapeSelectorView(Context context, AttributeSet attrs) {
   super(context, attrs);
   setupAttributes(attrs);
   setupPaint();
 }

 @Override
 protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
   if (displayShapeName) {
     canvas.drawText("Square", shapeWidth + textXOffset, shapeHeight + textXOffset, paintShape);
   }
 }

 private void setupPaint() { 
     paintShape = new Paint();
     paintShape.setStyle(Style.FILL);
     paintShape.setColor(shapeColor);
     paintShape.setTextSize(30);
  }
}

 

這段代碼就會根據XML裡設置的shapeColor來畫圖,根據displayShapeName屬性來決定是否顯示圖形的名稱,結果如下圖:       更多畫圖的教程可以參考這裡 Custom 2D Drawing Tutorial   計算尺寸   為了更好的理解自定義視圖的寬度和高度,我們需要定義onMeasure方法,這個方法根據視圖的內容來決定它的寬度和高度,在這裡寬度和高度是由形狀和下面的文本決定的,如下所示:  
public class ShapeSelectorView extends View {
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Defines the extra padding for the shape name text
   int textPadding = 10;
   int contentWidth = shapeWidth;
   
   // Resolve the width based on our minimum and the measure spec
   int minw = contentWidth + getPaddingLeft() + getPaddingRight();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
   
   // Ask for a height that would let the view get as big as it can
   int minh = shapeHeight + getPaddingBottom() + getPaddingTop();
   if (displayShapeName) { 
minh += textYOffset + textPadding;
   }
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
   
   // Calling this method determines the measured width and height
   // Retrieve with getMeasuredWidth or getMeasuredHeight methods later
   setMeasuredDimension(w, h);
 }
}

 

寬度和高度都是基於MeasureSpec來討論的,一個MeasureSpec封裝了父布局傳遞給子布局的布局要求,每個MeasureSpec代表了一組寬度和高度的要求。一個MeasureSpec由大小和模式組成。它有三種模式:UNSPECIFIED(未指定),父元素未給子元素施加任何束縛,子元素可以得到任意想要的大小;EXACTLY(完全),父元素決定子元素的確切大小,子元素將被限定在給定的邊界裡而忽略它本身大小;AT_MOST(至多),子元素至多達到指定大小的值。resolveSizeAndState()方法根據視圖想要的大小和MeasureSpec返回一個合適的值,最後你需要調用setMeasureDimension()方法生效。   不同形狀之間切換   如果想實現用戶點擊之後改變形狀,需要在onTouchEvent方法裡添加自定義邏輯:  
public class ShapeSelectorView extends View {
 // ...
 private String[] shapeValues = { "square", "circle", "triangle" };
 private int currentShapeIndex = 0;

 // Change the currentShapeIndex whenever the shape is clicked
 @Override
 public boolean onTouchEvent(MotionEvent event) {
   boolean result = super.onTouchEvent(event);
   if (event.getAction() == MotionEvent.ACTION_DOWN) {
     currentShapeIndex ++;
     if (currentShapeIndex > (shapeValues.length - 1)) {
currentShapeIndex = 0;
     }
     postInvalidate();
     return true;
   }
   return result;
 }
}

 

現在不管什麼時候視圖被單擊,選擇的形狀的下標會改變,調用postInvalisate()方法後會顯示一個不同的形狀,接下來更新onDraw()方法來實現更改形狀的邏輯:  
public class ShapeSelectorView extends View {
 // ...

 @Override
 protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   String shapeSelected = shapeValues[currentShapeIndex];
   if (shapeSelected.equals("square")) {
     canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
     textXOffset = 0;
   } else if (shapeSelected.equals("circle")) {
     canvas.drawCircle(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, paintShape);
     textXOffset = 12;
   } else if (shapeSelected.equals("triangle")) {
     canvas.drawPath(getTrianglePath(), paintShape);
     textXOffset = 0;
   }
   if (displayShapeName) {
     canvas.drawText(shapeSelected, 0 + textXOffset, shapeHeight + textYOffset, paintShape);
   }
 }

 protected Path getTrianglePath() {
   Point p1 = new Point(0, shapeHeight), p2 = null, p3 = null;
   p2 = new Point(p1.x + shapeWidth, p1.y);
   p3 = new Point(p1.x + (shapeWidth / 2), p1.y - shapeHeight);
   Path path = new Path();
   path.moveTo(p1.x, p1.y);
   path.lineTo(p2.x, p2.y);
   path.lineTo(p3.x, p3.y);
   return path;
 }

 // ...
}

 

現在每次點擊都會顯示一個不同的形狀,結果如下:       接下來添加一個獲取形狀的方法:  
public class ShapeSelectorView extends View {
 // ...
 // Returns selected shape name
 public String getSelectedShape() {
   return shapeValues[currentShapeIndex];
 }
}

 

保存視圖的狀態   當配置發生改變的時候(比如屏幕旋轉)視圖需要保存它們的狀態,你可以實現onSaveInstanceState()和onRestoreInstanceState()方法來保存和恢復視圖狀態,如下所示:  
public class ShapeSelectorView extends View {
 // This is the view state for this shape selector
 private int currentShapeIndex = 0;

 @Override
 public Parcelable onSaveInstanceState() {
   // Construct bundle
   Bundle bundle = new Bundle();
   // Store base view state
   bundle.putParcelable("instanceState", super.onSaveInstanceState());
   // Save our custom view state to bundle
   bundle.putInt("currentShapeIndex", this.currentShapeIndex);
   // ... store any other custom state here ...
   // Return the bundle
   return bundle;
 }

 @Override
 public void onRestoreInstanceState(Parcelable state) {
   // Checks if the state is the bundle we saved
   if (state instanceof Bundle) {
     Bundle bundle = (Bundle) state;
     // Load back our custom view state
     this.currentShapeIndex = bundle.getInt("currentShapeIndex");
     // ... load any other custom state here ...
     // Load base view state back
     state = bundle.getParcelable("instanceState");
   }
   // Pass base view state on to super
   super.onRestoreInstanceState(state);
 }
}

 

一旦你實現了這些保存和恢復的邏輯,當手機配置改變的時候你的視圖能夠自動保存狀態。 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved