Heim  >  Artikel  >  Java  >  Beispiel-Tutorials zum Anpassen von View und ViewGroup in der Android-App-Entwicklung

Beispiel-Tutorials zum Anpassen von View und ViewGroup in der Android-App-Entwicklung

高洛峰
高洛峰Original
2017-01-16 16:30:421731Durchsuche

Ansicht
Alle Android-Steuerelemente sind Ansichten oder Unterklassen von Ansichten. Sie stellen tatsächlich einen rechteckigen Bereich auf dem Bildschirm dar, der durch ein Rechteck dargestellt wird. Links und oben stellen die Ansicht relativ zu ihrer übergeordneten Ansicht dar. und Höhe stellen die Breite und Höhe der Ansicht dar. Durch diese vier Felder kann die Position der Ansicht auf dem Bildschirm bestimmt werden. Nachdem Sie die Position bestimmt haben, können Sie mit dem Zeichnen des Inhalts der Ansicht beginnen.

View-Zeichnungsprozess
View-Zeichnung kann in die folgenden drei Prozesse unterteilt werden:

Messen
View führt zunächst eine Messung durch, um zu berechnen, wie viel Fläche es einnehmen muss. Der Measure-Prozess von View stellt uns eine Schnittstelle onMeasure zur Verfügung. Die Methodendefinition lautet wie folgt:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

Die View-Klasse hat eine grundlegende onMeasure-Implementierung bereitgestellt,

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
     getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
 int result = size;
 int specMode = MeasureSpec.getMode(measureSpec);
 int specSize = MeasureSpec.getSize(measureSpec);
 
 switch (specMode) {
 case MeasureSpec.UNSPECIFIED:
   result = size;
   break;
 case MeasureSpec.AT_MOST:
 case MeasureSpec.EXACTLY:
   result = specSize;
   break;
 }
 return result;
}

Die Methode setMeasuredDimension() wird aufgerufen, um die Breite und Höhe der Ansicht während des Messvorgangs festzulegen. getSuggestedMinimumWidth() gibt die minimale Breite der Ansicht zurück, und Height verfügt auch über eine entsprechende Methode. Kurz gesagt: Die MeasureSpec-Klasse ist eine interne statische Klasse der View-Klasse. Sie definiert die drei Konstanten UNSPECIFIED, AT_MOST und EXACTLY. Sie entsprechen match_parent, wrap_content und xxxdp in LayoutParams . Wir können onMeasure überschreiben, um die Breite und Höhe der Ansicht neu zu definieren.

Layout
Der Layout-Prozess ist für die View-Klasse sehr einfach. View stellt uns auch die onLayout-Methode zur Verfügung

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

Da wir jetzt über View sprechen, Es gibt keine Unteransichten, die angeordnet werden müssen, sodass wir in diesem Schritt eigentlich keine zusätzliche Arbeit leisten müssen. Übrigens müssen wir für die ViewGroup-Klasse in der onLayout-Methode die Größe, Breite und Höhe aller Unteransichten festlegen. Dies werden wir im nächsten Artikel ausführlich erklären.

Zeichnen
Der Zeichenvorgang besteht darin, den von uns benötigten Ansichtsstil auf die Leinwand zu zeichnen. In ähnlicher Weise stellt uns View die onDraw-Methode zur Verfügung

protected void onDraw(Canvas canvas) {
}

OnDraw der Standard-View-Klasse verfügt nicht über eine Codezeile, sondern stellt uns beispielsweise eine leere Leinwand zur Verfügung a Genau wie bei einer Bildrolle sind wir die Maler, und die Wirkung, die wir erzielen können, hängt ganz von uns ab.

Es gibt drei weitere wichtige Methoden in View
requestLayout
View ruft den Layoutprozess erneut auf.

invalidate
View ruft den Zeichenvorgang erneut auf.

forceLayout
zeigt an, dass die Ansicht das nächste Mal neu gezeichnet und der Layoutprozess erneut aufgerufen werden muss.

Benutzerdefinierte Attribute
Wir haben bereits den gesamten View-Zeichnungsprozess eingeführt, und es gibt noch ein weiteres sehr wichtiges Wissen: benutzerdefinierte Steuerattribute. Wir alle wissen, dass View bereits über einige grundlegende Attribute verfügt, wie z. B. „layout_width“, „layout_height“. Hintergrund usw. Wir müssen oft unsere eigenen Attribute definieren, damit Sie dies gezielt tun können.

1. Öffnen Sie im Werteordner attrs.xml. Der Name dieser Datei kann hier beliebig sein, was bedeutet, dass alle Attribute der Ansicht vorhanden sind werden darin platziert.
2. Da in unserem folgenden Beispiel zwei Längenattribute und ein Farbwert verwendet werden, erstellen wir hier zunächst drei Attribute.

<declare-styleable name="rainbowbar">
 <attr name="rainbowbar_hspace" format="dimension"></attr>
 <attr name="rainbowbar_vspace" format="dimension"></attr>
 <attr name="rainbowbar_color" format="color"></attr>
</declare-styleable>

Wie man es nutzt, schauen wir uns ein Beispiel an.

Implementieren Sie einen relativ einfachen Google Rainbow-Fortschrittsbalken.
Der Einfachheit halber verwende ich hier nur eine Farbe und überlasse Ihnen mehrere Farben. Gehen wir direkt zum Code.

public class RainbowBar extends View {
 
 //progress bar color
 int barColor = Color.parseColor("#1E88E5");
 //every bar segment width
 int hSpace = Utils.dpToPx(80, getResources());
 //every bar segment height
 int vSpace = Utils.dpToPx(4, getResources());
 //space among bars
 int space = Utils.dpToPx(10, getResources());
 float startX = 0;
 float delta = 10f;
 Paint mPaint;
 
 public RainbowBar(Context context) {
  super(context);
 }
 
 public RainbowBar(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
 }
 
 public RainbowBar(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  //read custom attrs
  TypedArray t = context.obtainStyledAttributes(attrs,
      R.styleable.rainbowbar, 0, 0);
  hSpace = t.getDimensionPixelSize(R.styleable.rainbowbar_rainbowbar_hspace, hSpace);
  vSpace = t.getDimensionPixelOffset(R.styleable.rainbowbar_rainbowbar_vspace, vSpace);
  barColor = t.getColor(R.styleable.rainbowbar_rainbowbar_color, barColor);
  t.recycle();  // we should always recycle after used
  mPaint = new Paint();
  mPaint.setAntiAlias(true);
  mPaint.setColor(barColor);
  mPaint.setStrokeWidth(vSpace);
 }
 
 .......
}

View verfügt über drei Konstruktionsmethoden, die wir neu schreiben müssen. Hier sind die Szenarien, in denen die drei Methoden aufgerufen werden.

Die erste Methode , es wird aufgerufen, wenn wir es wie folgt verwenden: View view = new View(context);
Die zweite Methode, wenn wir View in der XML-Layoutdatei verwenden, wird während des Inflate-Layouts aufgerufen,
c1d4b0d7c21f209d7500db600843c7d1.
Die dritte Methode ähnelt der zweiten Methode, fügt jedoch die Stilattributeinstellung hinzu. Zu diesem Zeitpunkt wird der dritte Konstruktor während des Inflater-Layouts aufgerufen.
a298b83783294fb1ecb90b876eada1d0.
Was Sie oben vielleicht etwas verwirrt, ist, dass ich den Code zum Initialisieren und Lesen der benutzerdefinierten Attribute hspace, vspace und barcolor in der dritten Konstruktionsmethode geschrieben habe, meine RainbowBar jedoch das style-Attribut im linearen Layout nicht hinzugefügt hat . (), gemäß unserer obigen Erklärung, sollte der zweite Konstruktor beim Aufblasen des Layouts aufgerufen werden, aber wir haben den dritten Konstruktor im zweiten Konstruktor aufgerufen, also this(context, attrs, 0); Es gibt also kein Problem beim Lesen der benutzerdefinierten Attribute In der dritten Konstruktionsmethode ist dies ein kleines Detail, um Code-Redundanz zu vermeiden Verfahren.


//draw be invoke numbers.
int index = 0;
@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  //get screen width
  float sw = this.getMeasuredWidth();
  if (startX >= sw + (hSpace + space) - (sw % (hSpace + space))) {
    startX = 0;
  } else {
    startX += delta;
  }
  float start = startX;
  // draw latter parse
  while (start < sw) {
    canvas.drawLine(start, 5, start + hSpace, 5, mPaint);
    start += (hSpace + space);
  }
 
  start = startX - space - hSpace;
 
  // draw front parse
  while (start >= -hSpace) {
    canvas.drawLine(start, 5, start + hSpace, 5, mPaint);
    start -= (hSpace + space);
  }
  if (index >= 700000) {
    index = 0;
  }
  invalidate();
}
Layoutdatei:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout   xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="40dp"
android:orientation="vertical" >
 
<com.sw.demo.widget.RainbowBar
    android:layout_width="match_parent"
  android:layout_height="wrap_content"
  app:rainbowbar_color="@android:color/holo_blue_bright"
  app:rainbowbar_hspace="80dp"
  app:rainbowbar_vspace="10dp"
  ></com.sw.demo.widget.RainbowBar>
 
</LinearLayout>
Tatsächlich wird die Methode drawLine von Canvas aufgerufen und dann verschoben Beginnen wir jedes Mal mit dem Vorwärtsziehen. Am Ende der Methode rufen wir die Methode „Invalidate“ auf. Wie wir oben erklärt haben, veranlasst diese Methode die Ansicht, die Methode „onDraw“ erneut aufzurufen, sodass wir den Effekt unseres Fortschrittsbalkens erzielen nach vorne gezogen. Unten ist der endgültige Anzeigeeffekt zu sehen, wenn er in ein GIF umgewandelt wird, aber der tatsächliche Effekt ist Blau. Wir haben nur ein paar Dutzend Codezeilen geschrieben. Das Anpassen der Ansicht ist nicht so schwierig, wie wir es uns vorgestellt haben. Im nächsten Artikel werden wir den Zeichenprozess von ViewGroup weiter erlernen.


自定义ViewGroup
ViewGroup
我们知道ViewGroup就是View的容器类,我们经常用的LinearLayout,RelativeLayout等都是ViewGroup的子类,因为ViewGroup有很多子View,所以它的整个绘制过程相对于View会复杂一点,但是还是三个步骤measure,layout,draw,我们一次说明。

Measure
Measure过程还是测量ViewGroup的大小,如果layout_widht和layout_height是match_parent或具体的xxxdp,就很简答了,直接调用setMeasuredDimension()方法,设置ViewGroup的宽高即可,如果是wrap_content,就比较麻烦了,我们需要遍历所有的子View,然后对每个子View进行测量,然后根据子View的排列规则,计算出最终ViewGroup的大小。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 int childCount = this.getChildCount();
 for (int i = 0; i < childCount; i++) {
   View child = this.getChildAt(i);
   this.measureChild(child, widthMeasureSpec, heightMeasureSpec);
   int cw = child.getMeasuredWidth();
   // int ch = child.getMeasuredHeight();
 }
}

你可能需要类似上面的代码,其中getChildCount()方法,返回子View的数量,measureChild()方法,调用子View的测量方法。

Layout
上面View的自定义中,我们稍微提到了,layout过程其实就是对子View的位置进行排列,onLayout方法给我一个机会,来按照我们想要的规则自定义子View排列。

@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
 int childCount = this.getChildCount();
 for (int i = 0; i < childCount; i++) {
   View child = this.getChildAt(i);
   LayoutParams lParams = (LayoutParams) child.getLayoutParams();
   child.layout(lParams.left, lParams.top, lParams.left + childWidth,
       lParams.top + childHeight);
 }
}

你同样可能需要类似上面的代码,其中child.layout(left,top,right,bottom)方法可以对子View的位置进行设置,四个参数的意思大家通过变量名都应该清楚了。
Draw
ViewGroup在draw阶段,其实就是按照子类的排列顺序,调用子类的onDraw方法,因为我们只是View的容器, 本身一般不需要draw额外的修饰,所以往往在onDraw方法里面,只需要调用ViewGroup的onDraw默认实现方法即可。

LayoutParams
ViewGroup还有一个很重要的知识LayoutParams,LayoutParams存储了子View在加入ViewGroup中时的一些参数信息,在继承ViewGroup类时,一般也需要新建一个新的LayoutParams类,就像SDK中我们熟悉的LinearLayout.LayoutParams,RelativeLayout.LayoutParams类等一样,那么可以这样做,在你定义的ViewGroup子类中,新建一个LayoutParams类继承与ViewGroup.LayoutParams。

public static class LayoutParams extends ViewGroup.LayoutParams {
 
 public int left = 0;
 public int top = 0;
 
 public LayoutParams(Context arg0, AttributeSet arg1) {
   super(arg0, arg1);
 }
 
 public LayoutParams(int arg0, int arg1) {
   super(arg0, arg1);
 }
 
 public LayoutParams(android.view.ViewGroup.LayoutParams arg0) {
   super(arg0);
 }
 
}

那么现在新的LayoutParams类已经有了,如何让我们自定义的ViewGroup使用我们自定义的LayoutParams类来添加子View呢,ViewGroup同样提供了下面这几个方法供我们重写,我们重写返回我们自定义的LayoutParams对象即可。

@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(
   AttributeSet attrs) {
 return new NinePhotoView.LayoutParams(getContext(), attrs);
}
 
@Override
protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
 return new LayoutParams(LayoutParams.WRAP_CONTENT,
     LayoutParams.WRAP_CONTENT);
}
 
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(
   android.view.ViewGroup.LayoutParams p) {
 return new LayoutParams(p);
}
 
@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
 return p instanceof NinePhotoView.LayoutParams;
}

   

实例
我们还是做一个实例来说明,我们今天做一个类似微信朋友圈 存储要发送图片的控件,点击+号图片,可以一直加图片,最多9张。那么微信是4个一排,我们这里是3个一排,因为一般常规都是三个一排,这些都是细节不要在意(另外偷偷告诉大家,微信的实现是用TableLayout,-.-)。

Android App开发中自定义View和ViewGroup的实例教程

public class NinePhotoView extends ViewGroup {
 
public static final int MAX_PHOTO_NUMBER = 9;
 
private int[] constImageIds = { R.drawable.girl_0, R.drawable.girl_1,
   R.drawable.girl_2, R.drawable.girl_3, R.drawable.girl_4,
   R.drawable.girl_5, R.drawable.girl_6, R.drawable.girl_7,
   R.drawable.girl_8 };
 
// horizontal space among children views
int hSpace = Utils.dpToPx(10, getResources());
// vertical space among children views
int vSpace = Utils.dpToPx(10, getResources());
 
// every child view width and height.
int childWidth = 0;
int childHeight = 0;
 
// store images res id
ArrayList<integer> mImageResArrayList = new ArrayList<integer>(9);
private View addPhotoView;
 
public NinePhotoView(Context context) {
 super(context);
}
 
public NinePhotoView(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
}
 
public NinePhotoView(Context context, AttributeSet attrs, int defStyle) {
 super(context, attrs, defStyle);
 
 TypedArray t = context.obtainStyledAttributes(attrs,
     R.styleable.NinePhotoView, 0, 0);
 hSpace = t.getDimensionPixelSize(
     R.styleable.NinePhotoView_ninephoto_hspace, hSpace);
 vSpace = t.getDimensionPixelSize(
     R.styleable.NinePhotoView_ninephoto_vspace, vSpace);
 t.recycle();
 
 addPhotoView = new View(context);
 addView(addPhotoView);
 mImageResArrayList.add(new integer());
}

目前为止,都跟上一篇说的大致差不多,另外拍照和从相册选择图片不是我们这一篇的重点,所以我们把图片硬编码到代码中(全是美女...),ViewGroup初始化时我们添加了一个+号按钮,给用户点击添加新的图片。

Measure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 int rw = MeasureSpec.getSize(widthMeasureSpec);
 int rh = MeasureSpec.getSize(heightMeasureSpec);
 
 childWidth = (rw - 2 * hSpace) / 3;
 childHeight = childWidth;
 
 int childCount = this.getChildCount();
 for (int i = 0; i < childCount; i++) {
   View child = this.getChildAt(i);
   //this.measureChild(child, widthMeasureSpec, heightMeasureSpec);
 
   LayoutParams lParams = (LayoutParams) child.getLayoutParams();
   lParams.left = (i % 3) * (childWidth + hSpace);
   lParams.top = (i / 3) * (childWidth + vSpace);
 }
 
 int vw = rw;
 int vh = rh;
 if (childCount < 3) {
   vw = childCount * (childWidth + hSpace);
 }
 vh = ((childCount + 3) / 3) * (childWidth + vSpace);
 setMeasuredDimension(vw, vh);
}

我们的子View三个一排,而且都是正方形,所以我们上面通过循环很好去得到所有子View的位置,注意我们上面把子View的左上角坐标存储到我们自定义的LayoutParams 的left和top二个字段中,Layout阶段会使用,最后我们算得整个ViewGroup的宽高,调用setMeasuredDimension设置。

Layout

@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
 int childCount = this.getChildCount();
 for (int i = 0; i < childCount; i++) {
   View child = this.getChildAt(i);
   LayoutParams lParams = (LayoutParams) child.getLayoutParams();
   child.layout(lParams.left, lParams.top, lParams.left + childWidth,
       lParams.top + childHeight);
 
   if (i == mImageResArrayList.size() - 1 && mImageResArrayList.size() != MAX_PHOTO_NUMBER) {
     child.setBackgroundResource(R.drawable.add_photo);
     child.setOnClickListener(new View.OnClickListener() {
 
       @Override
       public void onClick(View arg0) {
         addPhotoBtnClick();
       }
     });
   }else {
     child.setBackgroundResource(constImageIds[i]);
     child.setOnClickListener(null);
   }
 }
}
 
public void addPhoto() {
 if (mImageResArrayList.size() < MAX_PHOTO_NUMBER) {
   View newChild = new View(getContext());
   addView(newChild);
   mImageResArrayList.add(new integer());
   requestLayout();
   invalidate();
 }
}
 
public void addPhotoBtnClick() {
 final CharSequence[] items = { "Take Photo", "Photo from gallery" };
 
 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
 builder.setItems(items, new DialogInterface.OnClickListener() {
 
   @Override
   public void onClick(DialogInterface arg0, int arg1) {
     addPhoto();
   }
 
 });
 builder.show();
}

最核心的就是调用layout方法,根据我们measure阶段获得的LayoutParams中的left和top字段,也很好对每个子View进行位置排列。然后判断在图片未达到最大值9张时,默认最后一张是+号图片,然后设置点击事件,弹出对话框供用户选择操作。

Draw
不需要重写,使用ViewGroup默认实现即可。
附上布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="40dp"
android:orientation="vertical" >
 
<com.sw.demo.widget.NinePhotoView
  android:id="@+id/photoview"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  app:ninephoto_hspace="10dp"
  app:ninephoto_vspace="10dp"
  app:rainbowbar_color="@android:color/holo_blue_bright" >
 
</com.sw.demo.widget.NinePhotoView>
 
</LinearLayout>

最后还是加上程序运行的效果图,今天自定义ViewGroup的讲解就这么多了,祝大家每天都有新收获,每天都有好心情~~~

更多Android App开发中自定义View和ViewGroup的实例教程相关文章请关注PHP中文网!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn