Android自定义View和控件之一-定制属于自己的UI

时间:2021-04-13 19:28:19

照例,拿来主义。我的学习是基于下面的三篇blog。前两是基本的流程,第三篇里有比较细致的绘制相关的属性。第4篇介绍了如何减少布局层次来提高效率。

1. 教你搞定Android自定义View

2. 教你搞定Android自定义ViewGroup

3. Android中自定义视图View之—前奏篇

4. 如何更好的通过Inflate layout的方式来实现自定义view

一般对于交互要求的特殊控件,有两种方式:

一是直接继承View或ViewGroup,从定义控件的绘制属性开始,在屏幕上绘制自定义控件。上面的三个blog已经介绍的比较详细了。

还有一种方式是直接继承现有的控件和布局,比如绘制带3D效果的画廊,要继承Gallery并重写相关方法,或者直接继承RelativeLayout、LinearLayout等现有布局,然后添加View。下面介绍一下我在使用这种方式下的一些总结。

下面的两个列子都属于View重复绘制,也就是在一个布局上绘制多次,当然可以用listview,gridview等实现,但有的位置或者动画这些原始控件无法做到,就需要自定义了。

比如继承RelativeLayout,然后添加View,这个添加也分两种:

1.可以直接在代码中用new的方式,然后用LayoutParams,Gravity等指定该控件的位置。

比如下面的OverlayLayout,是水平展示头像控件,要求相邻图片重叠1/4左右,并且前面的圆形头像压在后面的头像上面。这里采用从后向前的方式也就是从右向左的方式绘制,会保证最左面的头像在最上面。因为每次绘制只有一个ImageView添加,直接用new的方式创建即可。代码如下:

/*
* @Project: GZJK
* @Author: BMR
* @Date: 2015年9月8日
* @Copyright: 2000-2015 CMCC . All rights reserved.
*/
package com.demo.demomutiprogress; import java.util.ArrayList;
import java.util.List; import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.RelativeLayout; /**
* @ClassName: OverlayLayout
* @Description: 图片水平重叠控件
* @author BMR
* @date 2015年9月8日 下午4:04:56
*/ class OverlayLayout extends RelativeLayout { // =============================================================================
// Child views
// =============================================================================
// 初始化必须
private List<Drawable> mImgList = new ArrayList<Drawable>();//栈,最大下标显示在最开始,超出不显示
private int mWidth;
//属性读取必须
private int imageSize;
private int overlayWidth; private int maxCount; // 计算得出最大的图片显示个数
private int imgNum; // 实际显示的图片个数 private Context mContext; // =============================================================================
// Constructor
// =============================================================================
public OverlayLayout(Context context) {
this(context, null);
} public OverlayLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public OverlayLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.overlay);
imageSize = mTypedArray.getDimensionPixelSize(R.styleable.overlay_imageSize, 30);
overlayWidth = mTypedArray.getDimensionPixelSize(R.styleable.overlay_overlayWidth, 8);
this.mWidth = Tools.dip2px(mContext, Tools.getScreenWidth((Activity)mContext));
} /**
* 必须要调用的绘制函数,传入要显示的图片List和控件宽度
* List<Drawable> imgList
* int width
*/
public void initOverlay(List<Drawable> imgList, int width) {
this.mWidth = width;
this.mImgList = imgList;
drawOverlay();
} /**
* 更新绘制函数,传入要显示的图片List
* List<Drawable> imgList
* int width
*/
public void updateOverlay(List<Drawable> imgList) {
this.mImgList = imgList;
drawOverlay();
} public void drawOverlay() {
// 计算最多显示的图片数目
maxCount = (mWidth-overlayWidth) / (imageSize - overlayWidth);
// 计算实际要绘制的图片数目
imgNum = Math.min(maxCount, mImgList.size());
removeAllViews();
for (int i = 0; i < imgNum; i++) {
int index = mImgList.size() - imgNum + i;
int left = (imgNum - i - 1) * (imageSize - overlayWidth);
addDrawable(mImgList.get(index), left);
}
} private void addDrawable(Drawable drawable, int left){
ImageView imageView = new ImageView(mContext);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(imageSize,
imageSize);
int top = 0;
int right = 0;
int bottom = 0;
layoutParams.setMargins(left, top, right, bottom);
imageView.setLayoutParams(layoutParams);
imageView.setImageDrawable(drawable);
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
addView(imageView);
} }

如果用gridview也可以,但是只能实现后面的头像压着前面的。

2.如果添加View的是几个控件的组合,可以先用xml把这个view定义好,用Inflater获取整个的View。

有时控件比较多,直接new的方式然后指定位置会比较麻烦,此时可以预先将自己要绘制的View用xml定义好。构造函数里去填充。下面的代码定义了一个节点类View,包括一个背景,一个文本,然后一条连接线。

 class DotView extends RelativeLayout {

        // =============================================================================
// Child views
// =============================================================================
private TextView textView;
private ImageView imageView;
private View line;
private int index;
private boolean isLast = false; // =============================================================================
// Constructor
// ============================================================================= public DotView(Context context,int index) {
this(context, null);
setIndex(index);
} public DotView(Context context) {
this(context, null);
} public DotView(Context context, AttributeSet attrs) {
this(context, attrs,0);
} public DotView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(getContext(), R.layout.layout_dot_view, this);
this.textView = (TextView)findViewById(R.id.tv_dot);
this.imageView = (ImageView)findViewById(R.id.iv_dot);
this.line = (View)findViewById(R.id.vi_line);
setLastView(isLast); } public void setLastView(boolean isLast) {
this.isLast = isLast;
if (isLast) {
this.line.setVisibility(View.GONE);
}
}
public void setProperty(int currentDotNo) {
if(index < currentDotNo){
textView.setTextColor(textColorProgressed);
textView.setText("√");
Tools.setDrawableToBkg(textView, drawableProgressed);
line.setBackgroundColor(lineColorProgressed);
}else if(index == currentDotNo){
textView.setTextColor(Color.RED);
textView.setText("" + (index+1));
Tools.setDrawableToBkg(textView, drawableInprogress);
line.setBackgroundColor(lineColorUnprogress);
}
else {
textView.setTextColor(textColorUnprogress);
textView.setText("" + (index+1));
Tools.setDrawableToBkg(textView, drawableUnprogress);
line.setBackgroundColor(lineColorUnprogress);
}
} // =============================================================================
// Getters
// =============================================================================
public TextView getTextView() {
return textView;
} public ImageView getImageView() {
return imageView;
} public View getLine() {
return line;
} public boolean isLast() {
return isLast;
} public void setLast(boolean isLast) {
this.isLast = isLast;
} public int getIndex() {
return index;
} public void setIndex(int index) {
this.index = index; } }

对应的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dot_view"
android:layout_width="match_parent"
android:layout_height="50dp" > <ImageView
android:id="@+id/iv_dot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerInParent="true"
android:src="@drawable/ic_completed"
android:visibility="gone" /> <TextView
android:id="@+id/tv_dot"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentLeft="true"
android:layout_centerInParent="true"
android:background="@drawable/ic_completed"
android:gravity="center"
android:text="1"
android:textColor="@android:color/white" />
<View
android:id="@+id/vi_line"
android:layout_width="wrap_content"
android:layout_height="2dp"
android:layout_toRightOf="@id/tv_dot"
android:layout_centerInParent="true"
android:background="#999"
/> </RelativeLayout>

这里有一个注意点:

在DotView的构造函数里填充语句:

inflate(getContext(), R.layout.layout_dot_view, this);
this.textView = (TextView)findViewById(R.id.tv_dot);
this.imageView = (ImageView)findViewById(R.id.iv_dot);
this.line = (View)findViewById(R.id.vi_line);

而我一开始才用的是Adapter定义中那种方式,结果这个DotView怎么也不会绘制在页面上。

View view = LayoutInflater.from(context).inflate(R.layout.layout_dot_view, null);
this.textView = (TextView)view.findViewById(R.id.tv_dot);
this.imageView = (ImageView)view.findViewById(R.id.iv_dot);
this.line = (View)view.findViewById(R.id.vi_line);

在应用中自定义一个view,需要获取这个view的布局,需要用到(LinearLayout) LayoutInflater.from(context).inflate(R.layout.contentitem, null);

这个方法。一般的ListView等Adapter中的第二个参数会是一个null。通常情况下没有问题,但是如果我想给这个view设置一个对应的类,然后通过这个类来操作的话就会出问题。

一直以为都是一样的,跟到inflate源码才发现是因为第二种方式的选项,null该改为this,可以看到在inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)函数中,只有root不等于空的情况下才能够真正的把view添加到布局中。

看看参数root的含义:@param root Optional view to be the parent of the generated hierarchy

就是说这个表示的事view的容器是什么。如果不告诉SDK你要把这个view放到哪里,当然就不能生成view了。

View view = LayoutInflater.from(context).inflate(R.layout.layout_dot_view, this);

而对Adapter中getView方法中

可以参见该函数的注释:

Get a View that displays the data at the specified position in the data set. You can either create a View manually or inflate it from an XML layout file. When the View is inflated, the parent View (GridView, ListView...) will apply default layout parameters unless you use android.view.LayoutInflater.inflate(int, android.view.ViewGroup, boolean) to specify a root view and to prevent attachment to the root.

也就是如果不指定root,默认将父View指定为root。对于Adapter父View当然就是绑定这个适配器的View如ListView,GridView等。而上面我们自定义的View作为一个类是没有默认父View的布局去绑定的。

最后,附上自己改造的别人的例子,在Github上的地址:https://github.com/maoranbian/RanCustomAndroidUI

子文件夹:/DemoMutiProgress,里面添加了上述几种情况下的绘制的例子。

下一篇打算罗列一些动画绘制有关的blog。忠实的blog收集者终于要整理浏览器收藏夹了哈哈。