android 自定义控件分为两种:
- 通过view或者viewGroup重新onMeasure和onDraw实现指定UI的控件
- 通过包装xml布局文件实现特定功能的UI控件
方式一:自绘控件
使用自绘控件首先要具备基本的自绘知识,如果没有请自行前往补习。我们使用app的时候经常会记录搜索记录,界面如下:
首先该控件需要满足条件:如果当前行可以展示当前历史记录,则在一行展示,如果不满足则换行展示,所以需要实现高度wrap_content模式。
主要思路:
- 每条记录Item的高度是固定的,所以只需要知道某一条高度即可
- 在onMeasure里面优先计算子控件的高度和宽度,GONE模式的不计算
- 通过for循环计算容纳当前数量子控件的高度和宽度,从而推算出当前容器的高度
- 在onLayout里面对子控件进行布局
- 因为历史记录展示往往会限制数量,例如我们限制最多显示两行的历史记录
public class WrapContentControl extends ViewGroup {
public static interface IWarpItemClick{
public void onWarpItemClick(String text);
}
private int mMaxRow = 2;
public WrapContentControl(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
//计算每个子控件的高度和宽度
int nChildeCount = getChildCount();
for (int i = 0;i < nChildeCount;++i){
View child = getChildAt(i);
if(child.getVisibility() != View.GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
//计算容纳子控件需要的高度
int nItemHeight = 0;//Utils.dp2px(getContext(),35);
int nOffsetLeft = 0;
int nRow = 1;
for (int i = 0;i < nChildeCount;++i){
View child = getChildAt(i);
if(child.getVisibility() == View.GONE)
continue;
if(nItemHeight == 0)
nItemHeight = child.getMeasuredHeight();
if(nOffsetLeft + child.getMeasuredWidth() > wSpecSize ){
nOffsetLeft = 0;
nRow += 1;
}
nOffsetLeft += child.getMeasuredWidth();
if(nRow >= mMaxRow)
break;
}
int nHeight = nRow*nItemHeight;
//设置当前容器的宽度和高度
setMeasuredDimension(wSpecSize,nHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int nChildCount = getChildCount();
int nOffsetLeft = 0;
int nOffsetTop = 0;
int nRow = 1;
for (int i = 0;i < nChildCount;++i){
View child = getChildAt(i);
if(child.getVisibility() == View.GONE)
continue;
if(nOffsetLeft + child.getMeasuredWidth() > getWidth() ){
nOffsetLeft = 0;
nRow += 1;
nOffsetTop += (getHeight()/mMaxRow) *(nRow - 1);
}
//对子控件位置进行布局
child.layout(nOffsetLeft, nOffsetTop, nOffsetLeft + child.getMeasuredWidth(), nOffsetTop + child.getMeasuredHeight());
nOffsetLeft += child.getMeasuredWidth();
}
}
}
由于篇幅限制这里只做控件的自绘讲解,并不做数据更新等Adapter的适配讲解。如果偷懒的可以预先固定显示20个标签,然后根据数据动态更新内容和做数据显示即可。如下:
<com.stnts.rocket.Control.WrapContentControl
android:id="@+id/history_pannel"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/his_label_view" />
....这里一共20个item....
</com.stnts.rocket.Control.WrapContentControl>
根据数据表实时更新数据
public void update(List<String> histList){
//LayoutInflater.from(getContext()).inflate(R.layout.his_label_view,this,true);
int nCount = getChildCount();
for (int i = 0;i < histList.size();++i){
if(i < nCount){
View itemview = getChildAt(i);
itemview.setVisibility(VISIBLE);
TextView textView = itemview.findViewById(R.id.label_item);
textView.setText(histList.get(i));
}
}
for (int i = histList.size();i < nCount;++i){
View itemview = getChildAt(i);
itemview.setVisibility(GONE);
}
}
当然为了控件的实用性,建议写一个Adapter动态适配。
方式二:布局封装控件
主要思路:
- 通过继承ViewGroup或者其子类加载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="60dp">
<ImageView
android:id="@+id/back_control"
android:layout_height="30dp"
android:layout_width="30dp"
android:layout_gravity="center"
android:background="@drawable/icon_back"/>
<TextView
android:id="@+id/title_control"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="1"
android:text="导航标题"
android:gravity="center"
android:textSize="20sp"
android:textColor="@color/dark_sep"
android:singleLine="true" />
<View
android:layout_height="30dp"
android:layout_width="30dp"/>
</LinearLayout>
自定义控件
public class TitleBar extends RelativeLayout {
private TextView mTextView = null;
private ImageView mBackControl = null;
private Context mContext;
public TitleBar(Context context) {
super(context);
mContext = context;
}
public TitleBar(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init(context,attrs);
}
public TitleBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init(context,attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TitleBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mContext = context;
init(context,attrs);
}
void init(Context context,AttributeSet attrs) {
View view = View.inflate(context, R.layout.title,this);
TypedArray at = context.obtainStyledAttributes(attrs,R.styleable.TitleBar);
mTextView = view.findViewById(R.id.title_control);
mBackControl = view.findViewById(R.id.back_control);
mTextView.setText(at.getString(R.styleable.TitleBar_title));
mBackControl.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
((Activity)mContext).finish();
}catch (Exception e){
e.printStackTrace();
}
}
});
}
public void setTitle(String title){
if(mTextView != null){
mTextView.setText(title);
}
}
}
使用:
最终效果