当前位置: 移动技术网 > 移动技术>移动开发>Android > View体系和自定义View总结

View体系和自定义View总结

2020年07月24日  | 移动技术网移动技术  | 我要评论

参考Android进阶之光第三章

坐标系

Android坐标系:左上角为(0,0),右为x轴正方向,下为y轴正方向

View坐标系

  • getX()触摸点到当前View左侧的距离
  • getY()触摸点到当前View顶部的距离
  • getRawX()触摸点到屏幕左侧的距离
  • getRawY()触摸点到屏幕顶部的距离
  • getWidth()当前View的宽度
  • getHeight()当前View的高度
  • getLeft()当前View的左侧距离屏幕左侧的距离
  • getWidth = getRight() - getLeft();

有四种方式去改变View的位置

                layout(getLeft() + disX, getTop() + disY,
                        getRight() + disX, getBottom() + disY);//1
                
                //相当于对layout方法的一种封装
                offsetLeftAndRight(disX);
                offsetTopAndBottom(disY);//2
                
                //改变布局参数(父控件为LinearLayout)
                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + disX;
                layoutParams.topMargin = getTop() + disY;
                setLayoutParams(layoutParams);//3

                ViewGroup.MarginLayoutParams layoutParams1 = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams1.leftMargin = getLeft() + disX;
                layoutParams1.topMargin = getTop() + disY;
                setLayoutParams(layoutParams1);//4

全部代码(一个View实现随手指滑动而滑动)

public class CustomView extends View {
    private int lastX;
    private int lastY;

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int disX = x - lastX;
                int disY = y - lastY;
                layout(getLeft() + disX, getTop() + disY,
                        getRight() + disX, getBottom() + disY);//1

//                offsetLeftAndRight(disX);
//                offsetTopAndBottom(disY);//2
//
//
//                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
//                layoutParams.leftMargin = getLeft() + disX;
//                layoutParams.topMargin = getTop() + disY;
//                setLayoutParams(layoutParams);//3
//
//                ViewGroup.MarginLayoutParams layoutParams1 = (ViewGroup.MarginLayoutParams) getLayoutParams();
//                layoutParams1.leftMargin = getLeft() + disX;
//                layoutParams1.topMargin = getTop() + disY;
//                setLayoutParams(layoutParams1);//4
                break;
        }
        return true;
    }
}

ScrollTo与ScrollBy

((View)getParent()).scrollBy(-disX,-disY);

 这里为什么要设置为负值呢?

  • 因为我们的眼睛去看手机就如同放大镜去看报纸,如果想要看到左上角的内容,就需要移动放大镜到左上角,所以移动是放大镜在移动,而不是报纸在移动,所以为负值

Scroller

在使用ScrollTo和ScrollBy时,过程为瞬间完成的,为了实现过程的滑动,我们要用Scroller+View的computeScroll()进行配合

初始化Scroller 

public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

 系统会在绘制View的时候在draw()中调用该方法,在这个方法中,我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,每滚动一小段距离就调用invalidate()不断地进行重绘,重绘就会又调用该方法,这样就连贯起来实现了平滑移动的效果;

@Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

 

 这个是对外提供的一个方法,用来实现2秒内向右滑动一段距离

  public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mScroller.startScroll(scrollX, 0, delta, 0, 2000);
        invalidate();
    }

属性动画的使用

1.translatiionX和translationY: 平移

2.rotation旋转

3.PrivotX和PrivotY: 设定支点位置,默认支点为中心

4.alpha : 透明度

5.x和y :描述View的最终位置

实现点击旋转的效果

customView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "onClick: ");
                if (customView.getRotation() == 0 || customView.getRotation() == 360) {
                    ObjectAnimator.ofFloat(customView, "rotation", 0, 180)
                            .setDuration(2000).start();
                } else if (customView.getRotation() == 180) {
                    ObjectAnimator.ofFloat(customView, "rotation", 180, 360)
                            .setDuration(2000).start();
                }
            }
        });

 当多个动画效果时,使用一个set集合.with代表同时执行..after代表先执行哪一个动画

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(clickMe, "translationX", 0.0f, 200f, 0f);
                ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(clickMe,"alpha",1.0f,0.1f,1.0f);
                ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(clickMe,"scaleX",1.0f,0.8f,1.0f);
                ObjectAnimator objectAnimator3 = ObjectAnimator.ofFloat(clickMe, "rotation", 0f, 180f, 360f);

                AnimatorSet set = new AnimatorSet();
                set.setDuration(2000);
                set.play(objectAnimator).with(objectAnimator1).with(objectAnimator2).with(objectAnimator3);
                set.start();

3.View的事件分发机制

当点击事件产生后,事件首先会传递给当前的Activity,这会调用Activity的dispatchTouchEvent()方法,具体的事件处理工作都是交给PhoneWindow来完成的,然后PhoneWindow再把处理工作交给DecorView,之后再由DecorView将事件处理工作交给ViewGroup

事件分发的3个重要方法:

1.dispatchTouchEvent(MotionEvent ev)用于进行事件的分发

2.onInterceptTouchEvent(MotionEvent ev) 用于进行事件的拦截,在dispatchTouchEvent中调用,需要注意的是View中没有该方法;

3.onTouchEvent(MotionEvent ev) 用于处理点击事件,在dispatchTouchEvent中调用

一个完整的事件序列是由DOWN开始,UP结束,当ViewGroup要拦截事件的时候,那么后续的事件序列都交给他处理,而不用再调用onInterceptTouchEvent了.

1.onInterceptTouchEvent默认返回false,不进行拦截,所以想要拦截就要在自定义的ViewGroup重写这个方法

2.dispatchTouchEvent()使用for循环遍历子元素,这个for是倒序遍历的,即从最上层的view开始遍历,然后判断触摸点是否在子View的范围内或者子View是否在播放动画,均不符合遍历下一个子View,如果有子View,就调用子View的dispatchTouchEvent,如果ViewGroup没有子View,就调用super.dispatchTouchEvent

3.onTouchListener中的onTouch方法的优先级要高于onTouchEvent(),如果View调用了setOnclickListener和setOnLongClickListener,onTouchEvent就会返回true消耗这个事件;

总结一下:对于根ViewGroup,点击事件首先传递给他的dispatchTouchEvent(),如果该ViewGroup的onIterceptTouchEvent返回true,表示要拦截,就交给他的onTouchEvent进行处理,否则就交给子元素的dispatchTouchEvent进行处理,直到调用到最小的View的dispatchTouchEvent然后onTouchEvent进行处理,如果子View的onTouchEvent返回false,则传递给父View的onTouchEvent();

4.View的工作流程

指的是measure,layout,draw

1.MeasureSpec是View的内部类,封装了一个View的规格尺寸,在Measure的流程中,系统会将View的LayoutParams根据父容器所施加的规则转换为对应的MeasureSpec,然后在onMeasure方法中根据MeasureSpec来确定View的宽和高;SpecMode有以下三种:

  1. UNSPECIFIED:未指定模式,View想多大就多大,一般用于系统内部的测量
  2. AT_MOST:最大模式,对应于wrap_content属性,子View的大小是父View的SpecSize
  3. EXACTLY:精确模式,对应于match_parent属性和具体的数值,父容器的大小,也就是SpecSize的值;

2.View的measure流程

对于一个直接继承自View的自定义View来说,mrap_content和match_content的属性的效果是一样的,因此想要实现自定义View的wrap_content,则要重写onMeasure方法,并对自定义View的wrap_content属性进行处理

如果View设置了背景,View的size取mMinWidth和mBackground.getMinimumWidth()的最大值,minWidth可以指定,默认为0

3.ViewGroup的measure流程

不仅要测量自身,还要测量子元素的measure()方法,在Viewgroup中没有onMeasure()方法,却定义了measureChildren()方法,根据父容器的MeasureSpec模式结合子元素和LayoutParams属性来得出子元素的MeasureSpec属性,

比如LinearLayout的measure的流程,如果是垂直方向则调用measureVertical方法,否则就调用measureHorizontal方法,这里分析一下垂直方向上的,如果是WRAP_CONTENT,就将每个子元素的高度和margin垂直高度相加,最后加上padding的值

4.View的layout流程

ViewGroup的layout用来确定子元素的位置,View的layout用来确定自身的位置,layout里面的四个参数l,t,r,b分别是左,上,右,下相对于父容器的距离

比如,如果是垂直方向上childTop值就是不断累加的,这样子元素就会一个一个垂直方向排列

5.View的draw流程

  • 如果需要,绘制背景
  • 保存当前的canvas层(可跳过)
  • 绘制View的内容
  • 绘制子View
  • 如果需要,则绘制View的褪色边缘,类似于阴影效果(可跳过)
  • 绘制装饰,比如滚动条.

步骤1绘制背景调用了View 的drawBackground方法

步骤3绘制View的内容需要在自定义View重写onDraw()方法实现

步骤4ViewGroup调用dispatchDraw方法对子类View进行遍历,并调用子元素的draw方法,然后先判断是否有缓存,没有缓存就正常绘制,有就按缓存显示;

步骤6绘制装饰的方法View的onDrawFordground方法,用于绘制ScrollBar以及其他的装饰,并将它们绘制在视图内容的上层;

5.自定义View

分为继承系统控件(TextView)和继承View两种

1.继承系统控件的自定义View

public class InvalidTextView extends AppCompatTextView {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public InvalidTextView(Context context) {
        this(context, null);
    }

    public InvalidTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public InvalidTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth((float) 1.5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        canvas.drawLine(0, height / 2, width, height / 2, mPaint);
    }
}

2.继承View的自定义View

 

public class RectView extends View {
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int mColor;

    public RectView(Context context) {
        this(context, null);
    }

    public RectView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //在这里对自定义属性进行设计
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RectView);
        mColor = typedArray.getColor(R.styleable.RectView_rect_color, Color.GREEN);
        typedArray.recycle();
        initDraw();
    }

    private void initDraw() {
        mPaint.setColor(mColor);
        mPaint.setStrokeWidth((float) 1.5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        //对padding属性的修改
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width - paddingRight, height - paddingBottom, mPaint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //对于wrap_content,需要自己设定一个默认值,如果不设置,wrap_content和match_parent是一样 
        //的效果,因为之前说过,直接继承与View的自定义View,这两个属性是一样的   
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(600, 600);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(600, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 600);
        }
    }
}

 自定义属性:在values中创建attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RectView">
        <attr name="rect_color" format="color" />
    </declare-styleable>
</resources>

3.自定义ViewGroup

实现了这种一个ViewPager,并在里面添加了两个ListView

public class HorizontalView extends ViewGroup {
    private int lastInterceptX;
    private int lastInterceptY;
    private int lastX;
    private int lastY;
    //测量滑动速度
    private VelocityTracker mTracker;
    int currentIndex = 0;
    int childWidth = 0;
    private Scroller mScroller;
    private static final String TAG = "HorizontalView";

    public HorizontalView(Context context) {
        super(context);
        init();
    }

    public HorizontalView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //scroller初始化
        mScroller = new Scroller(getContext());
        //测速器初始化
        mTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //这里设置为false,onTouchEvent就无法收到了
                intercept = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int disX = x - lastInterceptX;
                int disY = y - lastInterceptY;
                //水平滑动时拦截,解决了滑动冲突
                if (Math.abs(disX) > Math.abs(disY)) {
                    //开始进入onTouchEvent事件
                    intercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        //这里设置lastX是为了在onTouchEvent收不到时也能更新
        lastX = x;
        lastY = y;
        lastInterceptX = x;
        lastInterceptY = y;
        return intercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //滑动过程中再次触摸时中断
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int disX = x - lastX;
                scrollBy(-disX, 0);
                break;
            case MotionEvent.ACTION_UP:
                //getScrollX为当前的View距离初始左上角的距离
                int distance = getScrollX() - childWidth * currentIndex;
                if (Math.abs(distance) > childWidth / 2) {
                    if (distance > 0) {
                        currentIndex++;
                    } else {
                        currentIndex--;
                    }
                } else {
                    mTracker.computeCurrentVelocity(1000);
                    float xVelocity = mTracker.getXVelocity();
                    if (Math.abs(xVelocity) > 50) {
                        if (xVelocity > 0) {
                            currentIndex--;
                        } else {
                            currentIndex++;
                        }
                    }
                }
                currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ?
                        getChildCount() - 1 : currentIndex;
                smoothScrollTo(currentIndex * childWidth, 0);
                mTracker.clear();
                break;
            default:
                break;
        }
        lastX = x;
        lastY = y;
        return true;
    }

    private void smoothScrollTo(int destX, int destY) {
        //起点x,起点y,距离x,距离y
        mScroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
        //重绘
        invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        //滑动是否还在继续
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    //确定子View的位置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for (int i = 0; i < childCount; i++) {
            child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                int measuredWidth = child.getMeasuredWidth();
                childWidth = measuredWidth;
                child.layout(left, 0, left + measuredWidth, child.getMeasuredHeight());
                //left要累加
                left += measuredWidth;
            }
        }
    }

    //要对wrap_content特殊处理,所以需要重写onMeasure,绘制子View
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //没有子元素
        if (getChildCount() == 0) {
            setMeasuredDimension(0, 0);
            //宽和高都为wrap_content
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int measuredWidth = childOne.getMeasuredWidth();
            int measuredHeight = childOne.getMeasuredHeight();
            setMeasuredDimension(measuredWidth * getChildCount(), measuredHeight);
            //只有宽是wrap_content
        } else if (widthMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int measuredWidth = childOne.getMeasuredWidth();
            setMeasuredDimension(measuredWidth * getChildCount(), heightSize);
            //只有高是wrap_content
        } else if (heightMode == MeasureSpec.AT_MOST) {
            int measuredHeight = getChildAt(0).getMeasuredHeight();
            setMeasuredDimension(widthSize, measuredHeight);
        }
    }
}
<com.example.a03view.HorizontalView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/lv_one"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <ListView
            android:id="@+id/lv_two"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.example.a03view.HorizontalView>
String[] str1 = new String[]{"1","2","3","1","2","3","1","2","3","1","2","3"};
        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
                this,android.R.layout.simple_list_item_1,str1);
        listView1.setAdapter(arrayAdapter);

        String[] str2 = new String[]{"a","b","c"};
        ArrayAdapter<String> arrayAdapter1 = new ArrayAdapter<String>(
                this,android.R.layout.simple_expandable_list_item_1,str2);
        listView2.setAdapter(arrayAdapter1);

 

 

 

 

 

 

 

 

 

 

 

本文地址:https://blog.csdn.net/qq873044564/article/details/107312399

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网