自定义View与ViewGroup

270 阅读9分钟

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,这个更为复杂后面有时间整理一下再做总结。