1、ViewGroup的职责是什么?
ViewGroup相当于一个放置View的容器,在写布局xml的时候,会告诉容器(凡是以layout开头的属性,都是为用于告诉容器),容器宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity),还有margin等。因此ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ,决定childView的位置。
为什么只是建议的宽和高,而不是直接确定呢,因为childView的宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。
2、View的职责是什么?
View的职责,根据测量模式和ViewGroup给出的建议宽和高,计算出自己的宽和高;另外还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形状。
3、ViewGroup和LayoutParams之间的关系?
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。
ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:
1、EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;
2、AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;
3、UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。
从API角度进行浅析:
View根据ViewGroup传入的测量值和模式,对自己宽高进行确定(onMeasure中完成),然后在onDraw中完成对自己的绘制。
ViewGroup需要给View传入view的测量值和模式(onMeasure中完成),而且对于此ViewGroup的父布局,自己也需要在onMeasure中完成对自己宽和高的确定。此外,需要在onLayout中完成对其childView的位置的指定。
自定义View:
1.关键部分
Drawing the layout is a two pass process: a measure pass and a layout pass (绘制布局是一个两道工序:测量通道和布局通道)
所以一个view执行OnDraw时最关键的是measure和layout。其实这很好理解的,一个view需要绘制出来,那么必须知道他要占多大的空间也就是measure,还得知道在哪里绘制,也就是把view放在哪里即layout。把这两部分掌握好也就可以随意自定义view了。至于viewGroup中如何绘制就参考官方文档,其实就是一个分发绘制,直到child是一个view自己进行绘制。
2、重写一个view一般情况下只需要重写onDraw()方法。那么什么时候需要重写onMeasure()、onLayout()、onDraw() 方法呢,这个问题只要把这几个方法的功能弄清楚就应该知道怎么做了。
①、如果需要绘制View的图像,那么需要重写onDraw()方法。(这也是最常用的重写方式。)
②、如果需要改变view的大小,那么需要重写onMeasure()方法。
③、如果需要改变View的(在父控件的)位置,那么需要重写onLayout()方法。
④、根据上面三种不同的需要你可以组合出多种重写方案。
3、按类型划分,自定义View的实现方式可分为三种:自绘控件、组合控件、以及继承控件。
自绘控件:自绘控件的意思就是,这个View上所展现的内容全部都是自己绘制出来的。
步骤:
1.绘制View:绘制View主要是onDraw()方法中完成。通过参数Canvas来处理,相关的绘制主要有drawRect、drawLine、drawPath等等。
Canvas绘制的常用方法:
drawColor() 填充颜色
drawLine() 绘制线
drawLines() 绘制线条
drawOval() 绘制圆
drawPaint()
drawPath() 绘制路径
drawPicture() 绘制图片
drawPoint() 绘制点
drawPoints() 绘制点
drawRGB() 填充颜色
drawRect() 绘制矩形
drawText() 绘制文本
drawTextOnPath() 在路径上绘制文本
2、刷新View :(刷新view的方法这里主要有:)
invalidate(int l,int t,int r,int b):刷新局部,四个参数分别为左、上、右、下
invalidate():整个view刷新。执行invalidate类的方法将会设置view为无效,最终重新调用onDraw()方法; invalidate()是用来刷新View的,必须是在UI线程中进行工作。在修改某个view的显示时,调用 invalidate()才能看到重新绘制的界面。invalidate()的调用是把之前的旧的view从主UI线程队列 中pop掉
invalidate(Rect dirty):刷新一个矩形区域
3、控制事件:(例如处理以下几个事件)
- onSaveInstanceState() 处理屏幕切换的现场保存事件
- onRestoreInstanceState() 屏幕切换的现场还原事件
- onKeyDown() 处理按键事件
- onMeasure() 当控件的父元素正要放置该控件时调用
示例代码:
<com.example.customview.view.CircleView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:background="@color/white"
app:circle_color="@color/black"/>
open class CircleView : View {
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs,0){
val a = context?.obtainStyledAttributes(attrs, R.styleable.CircleView)?.apply {
getColor(R.styleable.CircleView_circle_color, Color.RED) //Color.RED是默认值
}
a?.recycle()//资源回收
}
private val mPaint3 by lazy {
Paint().also {
it.color = Color.BLUE
it.style = Paint.Style.FILL
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val paddingLeft = paddingLeft
val paddingRight = paddingRight
val paddingTop = paddingTop
val paddingBottom = paddingBottom
val width = (width - paddingLeft - paddingRight).toFloat()
val height = (height - paddingTop - paddingBottom).toFloat()
val radius = Math.min(width,height) / 2
canvas?.drawCircle(paddingLeft + width/2,paddingTop + height/2,radius,mPaint3)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,200) //设置wrap_content属性时设置此默认的宽和高
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,heightSpecSize)
}else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,200)
}
}
}
组合控件:不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
示例代码:
<include
android:id="@+id/counterControlToolbar"
layout="@layout/common_toolbar_layout" />
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/common_blue_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#5297FF"
android:orientation="horizontal">
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:padding="10dp"
android:src="@mipmap/icon_back"
android:visibility="visible"
/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="报警"
android:textColor="#FEFEFE"
android:textSize="18sp"
/>
<ImageView
android:id="@+id/right_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:padding="10dp"
android:visibility="gone"
/>
<TextView
android:id="@+id/right_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:textSize="15sp"
android:padding="10dp"
android:textColor="@color/white"
android:text="确定"
android:visibility="gone"
/>
</RelativeLayout>
继承控件:我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。
示例代码:重写拦截触摸事件是的viewpage不能滑动fragment
class NoScrollViewPager(context: Context
,attrs:AttributeSet?) : ViewPager(context,attrs){
constructor(context: Context):this(context,null){
}
private var isCanScroll = false
fun setNoScroll(noScroll: Boolean) {
isCanScroll = noScroll
}
// 滑动到指定位置
override fun scrollTo(x: Int, y: Int) {
super.scrollTo(x, y)
}
// 触摸事件
override fun onTouchEvent(ev: MotionEvent?): Boolean {
return if (isCanScroll) {
super.onTouchEvent(ev)
} else {
false
}
}
// 设置当前显示的布局
override fun setCurrentItem(item: Int) {
super.setCurrentItem(item)
}
// 设置当前显示的布局,并定义滑动方式
override fun setCurrentItem(item: Int, smoothScroll: Boolean) {
super.setCurrentItem(item, smoothScroll)
}
// 拦截触摸事件
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return if (isCanScroll) {
super.onInterceptTouchEvent(ev)
} else {
false
}
}
}
备注:
1、getWidth(): View在设定好布局后整个View的宽度。
2、getMeasuredWidth():
对View上的内容进行测量后得到的View内容占据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法里调用measure(0,0);(measure中的参数的值你自己可以定义),否则你得到的结果和getWidth()得到的结果是一样的。
最近接触到一个新需求就是轮播图的效果,刚看到原型图的时候想到用两个recycleview实现,再通过handle实现轮播效果,但是两个recycleview如何嵌套使用呢? 答案是通过自定义viewgroup实现,因为recycleview其实也是个viewgroup所以和以上的实现办法有点不同。
创建的BannerLayout就是两个recycleview嵌套的viewgroup,可以直接在xml文件中使用
<com.example.library.banner.BannerLayout
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="200dp"
app:autoPlaying="true"
app:centerScale="1.3"
app:itemSpace="20"
app:moveSpeed="1.8"/>
public class BannerLayout extends FrameLayout {
private int autoPlayDuration;//刷新间隔时间
private boolean showIndicator;//是否显示指示器
private RecyclerView indicatorContainer;
private Drawable mSelectedDrawable;
private Drawable mUnselectedDrawable;
private IndicatorAdapter indicatorAdapter;
private int indicatorMargin;//指示器间距
private RecyclerView mRecyclerView;
private BannerLayoutManager mLayoutManager;
private int WHAT_AUTO_PLAY = 1000;
private boolean hasInit;
private int bannerSize = 1;
private int currentIndex;
private boolean isPlaying = false;
private boolean isAutoPlaying = true;
int itemSpace;
float centerScale;
float moveSpeed;
protected Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == WHAT_AUTO_PLAY) {
if (currentIndex == mLayoutManager.getCurrentPosition()) {
Log.d("ceshiyong","mLayoutManager.getCurrentPosition()" + mLayoutManager.getCurrentPosition());
++currentIndex;
mRecyclerView.smoothScrollToPosition(currentIndex);
mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, autoPlayDuration);
refreshIndicator();
}
}
return false;
}
});
public BannerLayout(Context context) {
this(context, null);
}
public BannerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}
protected void initView(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BannerLayout);
showIndicator = a.getBoolean(R.styleable.BannerLayout_showIndicator, true);
autoPlayDuration = a.getInt(R.styleable.BannerLayout_interval, 4000);
isAutoPlaying = a.getBoolean(R.styleable.BannerLayout_autoPlaying, true);
itemSpace = a.getInt(R.styleable.BannerLayout_itemSpace, 20);
centerScale = a.getFloat(R.styleable.BannerLayout_centerScale, 1.2f);
moveSpeed = a.getFloat(R.styleable.BannerLayout_moveSpeed, 1.0f);
if (mSelectedDrawable == null) {
//绘制默认选中状态图形
GradientDrawable selectedGradientDrawable = new GradientDrawable();
selectedGradientDrawable.setShape(GradientDrawable.OVAL);
selectedGradientDrawable.setColor(Color.RED);
selectedGradientDrawable.setSize(dp2px(5), dp2px(5));
selectedGradientDrawable.setCornerRadius(dp2px(5) / 2);
mSelectedDrawable = new LayerDrawable(new Drawable[]{selectedGradientDrawable});
}
if (mUnselectedDrawable == null) {
//绘制默认未选中状态图形
GradientDrawable unSelectedGradientDrawable = new GradientDrawable();
unSelectedGradientDrawable.setShape(GradientDrawable.OVAL);
unSelectedGradientDrawable.setColor(Color.GRAY);
unSelectedGradientDrawable.setSize(dp2px(5), dp2px(5));
unSelectedGradientDrawable.setCornerRadius(dp2px(5) / 2);
mUnselectedDrawable = new LayerDrawable(new Drawable[]{unSelectedGradientDrawable});
}
indicatorMargin = dp2px(4);
int marginLeft = dp2px(16);
int marginRight = dp2px(0);
int marginBottom = dp2px(11);
int gravity = GravityCompat.START;
int o = a.getInt(R.styleable.BannerLayout_orientation, 0);
int orientation = 0;
if (o == 0) {
orientation = OrientationHelper.HORIZONTAL;
} else if (o == 1) {
orientation = OrientationHelper.VERTICAL;
}
a.recycle();
//轮播图部分
mRecyclerView = new RecyclerView(context);
LayoutParams vpLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
addView(mRecyclerView, vpLayoutParams);
mLayoutManager = new BannerLayoutManager(getContext(), orientation);
mLayoutManager.setItemSpace(itemSpace);
mLayoutManager.setCenterScale(centerScale);
mLayoutManager.setMoveSpeed(moveSpeed);
mRecyclerView.setLayoutManager(mLayoutManager);
new CenterSnapHelper().attachToRecyclerView(mRecyclerView);
//指示器部分
indicatorContainer = new RecyclerView(context);
LinearLayoutManager indicatorLayoutManager = new LinearLayoutManager(context, orientation, false);
indicatorContainer.setLayoutManager(indicatorLayoutManager);
indicatorAdapter = new IndicatorAdapter();
indicatorContainer.setAdapter(indicatorAdapter);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.BOTTOM | gravity;
params.setMargins(marginLeft, 0, marginRight, marginBottom);
addView(indicatorContainer, params);
if (!showIndicator) {
indicatorContainer.setVisibility(GONE);
}
}
// 设置是否禁止滚动播放
public void setAutoPlaying(boolean isAutoPlaying) {
this.isAutoPlaying = isAutoPlaying;
setPlaying(this.isAutoPlaying);
}
public boolean isPlaying() {
return isPlaying;
}
//设置是否显示指示器
public void setShowIndicator(boolean showIndicator) {
this.showIndicator = showIndicator;
indicatorContainer.setVisibility(showIndicator ? VISIBLE : GONE);
}
//设置当前图片缩放系数
public void setCenterScale(float centerScale) {
this.centerScale = centerScale;
mLayoutManager.setCenterScale(centerScale);
}
//设置跟随手指的移动速度
public void setMoveSpeed(float moveSpeed) {
this.moveSpeed = moveSpeed;
mLayoutManager.setMoveSpeed(moveSpeed);
}
//设置图片间距
public void setItemSpace(int itemSpace) {
this.itemSpace = itemSpace;
mLayoutManager.setItemSpace(itemSpace);
}
/**
* 设置轮播间隔时间
*
* @param autoPlayDuration 时间毫秒
*/
public void setAutoPlayDuration(int autoPlayDuration) {
this.autoPlayDuration = autoPlayDuration;
}
public void setOrientation(int orientation) {
mLayoutManager.setOrientation(orientation);
}
/**
* 设置是否自动播放(上锁)
*
* @param playing 开始播放
*/
protected synchronized void setPlaying(boolean playing) {
if (isAutoPlaying && hasInit) {
if (!isPlaying && playing) {
mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, autoPlayDuration);
isPlaying = true;
} else if (isPlaying && !playing) {
mHandler.removeMessages(WHAT_AUTO_PLAY);
isPlaying = false;
}
}
}
/**
* 设置轮播数据集
*/
public void setAdapter(RecyclerView.Adapter adapter) {
hasInit = false;
mRecyclerView.setAdapter(adapter);
bannerSize = adapter.getItemCount();
mLayoutManager.setInfinite(bannerSize >= 3);
setPlaying(true);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0) {
setPlaying(false);
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
int first = mLayoutManager.getCurrentPosition();
Log.d("xxx", "onScrollStateChanged");
if (currentIndex != first) {
currentIndex = first;
}
if (newState == SCROLL_STATE_IDLE) {
setPlaying(true);
}
refreshIndicator();
}
});
hasInit = true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
setPlaying(false);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setPlaying(true);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setPlaying(true);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setPlaying(false);
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == VISIBLE) {
setPlaying(true);
} else {
setPlaying(false);
}
}
/**
* 标示点适配器
*/
protected class IndicatorAdapter extends RecyclerView.Adapter {
int currentPosition = 0;
public void setPosition(int currentPosition) {
this.currentPosition = currentPosition;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ImageView bannerPoint = new ImageView(getContext());
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
lp.setMargins(indicatorMargin, indicatorMargin, indicatorMargin, indicatorMargin);
bannerPoint.setLayoutParams(lp);
return new RecyclerView.ViewHolder(bannerPoint) {
};
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ImageView bannerPoint = (ImageView) holder.itemView;
bannerPoint.setImageDrawable(currentPosition == position ? mSelectedDrawable : mUnselectedDrawable);
}
@Override
public int getItemCount() {
return bannerSize;
}
}
protected int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
Resources.getSystem().getDisplayMetrics());
}
/**
* 改变导航的指示点
*/
protected synchronized void refreshIndicator() {
if (showIndicator && bannerSize > 1) {
indicatorAdapter.setPosition(currentIndex % bannerSize);
indicatorAdapter.notifyDataSetChanged();
}
}
public interface OnBannerItemClickListener {
void onItemClick(int position);
}
}
在外部调用的时候直接给BannerLayout设置Adapter就行了
mAdapter1 = WebBannerAdapter(this,list)
mAdapter1!!.setOnBannerItemClickListener{position ->
Toast.makeText(this@LoginActivity, "点击了第 $position 项", Toast.LENGTH_SHORT).show()
}
recycler.setAdapter(mAdapter1)
除了自定义view和viewgroup之外,关于recycleview还可以自定义RecyclerView.LayoutManager,这个更为复杂后面有时间整理一下再做总结。