IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 自定义View总结(2) -> 正文阅读

[移动开发]Android 自定义View总结(2)

说到那些炫酷的自定义View,就离不开动画。

属性动画和硬件加速

属性动画

ViewPropertyAnimator

1.使用View.animate()创建对象,以及使用ViewPropertyAnimator.translationX()等方法来设置动画;

2.可以连续调用来设置多个动画;

3.可以用setDuration()来设置持续时间;

4.可以用setStartDelay()来设置开始延时;

ObjectAnimator

使用ObjectAnimator.ofxxx()来创建对象,以及使用ObjectAnimator.statr()来主动启动动画。它的优势在于,可以为自定义属性设置动画。

ObjectAnimator animator = ObjectAnimator.ofObject(view, "radius", Utils.dp2px(200));

另外,自定义属性需要设置?getter??setter?方法,并且?setter?方法里需要调用 触发重绘:

public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius; invalidate();
}

可以使用 setduration()来设置持续时间;

可以用 setStartDelay()来设置开始延时;

以及其他一些便捷方法。

Interpolator

插值?,用于设置时间完成度到动画完成度的计算公式,直白地说即设置动画的速度曲线,通过setInterpolator(Interpolator)方法来设置.

常用的有

AccelerateDecelerateInterpolator:开始与结束的地方速率改变比较慢,在中间的时候加速

AccelerateInterpolator:开始的地方速率改变比较慢,然后开始加速

DecelerateInterpolator:在开始的地方快然后慢

LinearInterpolator:?以常量速率改变

PropertyValuesHolder

用于设置更加详细的动画,例如多个属性应用于同一个对象:

PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("radius", Utils.dp2px(200));
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("offset", Utils.dp2px(100));
ObjectAnimator animator = PropertyValuesHolder.ofPropertyValuesHolder(view, holder1, holder2);

或者,配合使用 ,对一个属性分多个段:

Keyframe keyframe1 = Keyframe.ofFloat(0, Utils.dpToPixel(100)); 
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, Utils.dpToPixel(250));
 Keyframe keyframe3 = Keyframe.ofFloat(1, Utils.dpToPixel(200)); 
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("radius", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder);

AnimatorSet?

将多个 Animator 合并在一起使用,先后顺序或并列顺序都可以:


AnimatorSet animatorSet = new AnimatorSet(); 
animatorSet.playTogether(animator1, animator2); 
animatorSet.start();

?TypeEvaluator

用于设置动画完成度到属性具体值的计算公式。默认的offInt()和ofFloat()已经有了自带的IntEvaluator和FloatEvaluator,但有的时候需要自己设置 Evaluator。例如,对于颜色,需要int 类型的颜色设置 ArgbEvaluator,而不是让它们使用 IntEvaluator

animator.setEvaluator(new ArgbEvaluator());

?

硬件加速

硬件加速是什么

1.使用 CPU 绘制到 Bitmap,然后把 Bitmap 贴到屏幕,就是软件绘制;

2.使用 CPU?把绘制内容转换成 GPU?操作,交给 GPU,由 GPU?负责真正的绘制,就叫硬件绘制;

3.使用 GPU 绘制就叫做硬件加速

怎么就加速了?

?1.GPU 分摊了工作

2.GPU 绘制简单图形(例如方形、圆形、直线)在硬件设计上具有先天优势,会更快

?3.流程得到优化(重绘流程涉及的内容更少)

硬件加速的缺陷:

兼容性。由于使用 GPU 的绘制(暂时)无法完成某些绘制,因此对于一些特定的 API,需要关闭硬件加速来转回到使用 CPU 进行绘制。

离屏缓冲:

1.离屏缓冲是什么:单独的一个绘制 View(或 View 的一部分)的区域

2.setLayerType() saveLayer()

2.setLayerType() 是对整个 View,不能针对 onDraw() 里面的某一具体过程

3.这个方法常用来关闭硬件加速,但它的定位和定义都不只是一个「硬件加速开关」。 ?它的作用是为绘制设置一个离屏缓冲,让后面的绘制都单独写在这个离屏缓冲内。如果参数填写LAYER_TYPE_SOFTWARE,会把离屏缓冲设置为一个 Bitmap ,即使用软件绘制来进行缓冲,这样就导致在设置离屏缓冲的同时,将硬件加速关闭了。但需要知道,这个方法被用来关闭硬件加速,只是因为 Android?并没有提供一个便捷的方法在 View?级别简单地开关硬件加速而已。

4.saveLayer() 是针对 Canvas 的,所以在 onDraw() 里可以使用 saveLayer() 来圈出具体哪部分绘制要用离屏缓冲

5.然而……最新的文档表示这个方法太重了,能不用就别用,尽量用 setLayerType()?代替。

自定义布局

布局过程

1.确定每个 View?的位置和尺寸 作用:为绘制和触摸范围做支持

?2.绘制:知道往哪里绘制

?3.触摸反馈:知道用户点的是哪里

流程

1.从整体看:

测量流程:从根 View 递归调用每一级子 View measure() 方法,对它们进行测量

布局流程:从根 View 递归调用每一级子 View layout() 方法,把测量过程得出的子 View的位置和尺寸传给子 View,子 View?保存

2.从个体看,对于每个 View

  1. 运行前,开发者在 xml?文件里写入对 View?的布局要求 layout_xxx
  2. View?在自己的 onMeasure() 中,根据开发者在 xml?中写的对子 View?的要求,和自己的可用空间,得出对子 View?的具体尺寸要求
  3. View?在自己的 onMeasure() 中,根据自己的特性算出自己的期望尺寸如果是 ViewGroup,还会在这里调用每个子 View measure() 进行测量
  4. View?在子 View?计算出期望尺寸后,得出子 View?的实际尺寸和位置
  5. View?在自己的 layout()?方法中,将父 View?传进来的自己的实际尺寸和位置保存(如果是 ViewGroup,还会在 onLayout() 里调用每个字 View layout() 把它们的尺寸位置传给它们)

具体开发

继承已有的 View,简单改写它们的尺寸:SquareImageView

  1. 重写 onMeasure()
  2. getMeasuredWidth() getMeasuredSize() 获取到测量出的尺寸
  3. 计算出最终要的尺寸
  4. setMeasuredDimension(width, height)?把结果保存
public class SquareImageView extends androidx.appcompat.widget.AppCompatImageView {
    public SquareImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();
        int size = Math.max(measuredWidth, measuredHeight);

        setMeasuredDimension(size, size); // 保存测得的尺寸
    }
}

对自定义 View 完全进行自定义尺寸计算:重写 onMeasure()CircleView

  1. 重写 onMeasure()
  2. 计算出自己的尺寸
  3. resolveSize() 或者 resolveSizeAndState() 修正结果

3.1resolveSize() / resolveSizeAndState() 内部实现

3.1.1首先用 MeasureSpec.getMode(measureSpec) MeasureSpec.getSize(measureSpec) 取出父 对自己的尺寸限制类型和具体限制尺寸;

3.1.2如果 measure spec mode EXACTLY,表示父 View 对子 View 的尺寸做出了精确限制,所以就放弃计算出的 size,直接选用 measure spec size

3.1.3如果 measure spec mode AT_MOST,表示父 View 对子 View 的尺寸只限制了上限,需要看情况:

(1)如果计算出的 size 不大于 spec 中限制的 size,表示尺寸没有超出限制,所以选用计算出的 size

(2)而如果计算出的 size 大于 spec 中限制的 size,表示尺寸超限了,所以选用spec size,并且在 resolveSizeAndState() 中会添加标志MEASURED_STATE_TOO_SMALL这个标志可以辅助父 View 做测量和布局的计算;

(3)如果 measure spec mode UNSPECIFIED,表示父 View 对子 View 没有任何尺寸限制,所以直接选用计算出的 size,忽略 spec 中的 size

3.1.4使用 setMeasuredDimension(width, height)?保存结果

public class CircleView extends View {
    private static final int RADIUS = (int) Utils.dpToPixel(80);
    private static final int PADDING = (int) Utils.dpToPixel(30);

    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = (PADDING + RADIUS) * 2;
        int height = (PADDING + RADIUS) * 2;

        width = resolveSizeAndState(width, widthMeasureSpec, 0);
        height = resolveSizeAndState(height, widthMeasureSpec, 0);
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawColor(Color.RED);
        canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, paint);
    }
}

?

自定义 Layout:重写 onMeasure() onLayout()TagLayout

1.重写onMeasure()

(1)遍历每个子 View,用 measureChildWidthMargins()?测量子 View

(2)需要重写 generateLayoutParams() 并返回 MarginLayoutParams 才能measureChildWithMargins() 方法

(3)有些子 View 可能需要重新测量(比如换行处)

(4)测量完成后,得出子 View 的实际位置和尺寸,并暂时保存

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthUsed = 0;
        int heightUsed = 0;
        int lineWidthUsed = 0;
        int lineMaxHeight = 0;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specWidth = MeasureSpec.getSize(widthMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            if (specMode != MeasureSpec.UNSPECIFIED &&
                    lineWidthUsed + child.getMeasuredWidth() > specWidth) {
                lineWidthUsed = 0;
                heightUsed += lineMaxHeight;
                lineMaxHeight = 0;
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            }
            Rect childBound;
            if (childrenBounds.size() <= i) {
                childBound = new Rect();
                childrenBounds.add(childBound);
            } else {
                childBound = childrenBounds.get(i);
            }
            childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
            lineWidthUsed += child.getMeasuredWidth();
            widthUsed = Math.max(widthUsed, lineWidthUsed);
            lineMaxHeight = Math.max(lineMaxHeight, child.getMeasuredHeight());
        }

        int width = widthUsed;
        int height = heightUsed + lineMaxHeight;
        setMeasuredDimension(width, height);
    }

?2.重写 onLayout()

  1. 如果开发者写了具体值(例如 layout_width="24dp"),就不用再考虑父View?的剩余空间了,直接用 LayoutParams.width?/?height?来作为子 View 的限制 size,而限制 mode?EXACTLY(为什么?课堂上说过,因为软件的直接开发者——xml?布局文件的编写者——的意见最重要,发生冲突的时候应该以开发者的意见为准。换个角度说,如果真的由于冲突导致界面 ?不正确,开发者可以通过修改 xml?文件来解决啊,所以开发者的意见是第一位,但你如果设计成冲突时开发者的意见不在第一位,就会导致软件的可 ?配置性严重降低);
  2. 如果开发者写的是 MATCH_PARENT,即要求填满父控件的可用空间,那么由于自己的可用空间和自己的两个 MeasureSpec?有关,所以需要根据自己widthMeasureSpec?heightMeasureSpec?中的 mode?来分情况判 断:
  3. 如果开发者写的是 WRAP_CONTENT,即要求子 View?不超限制的前提下
    1. 如果自己的 spec?中的 mode?UNSPECIFIED,说明自己的尺寸没有上限,那么让子 View?填满自己的可用空间就无从说起,因此选用退让方案:给子 View?限制的 mode?就设置为 UNSPECIFIEDsize?0?就好;
    2. 如果自己的 spec?中的 mode?EXACTLY?或者 AT_MOST,说明自己的尺寸有上限,那么把 spec?中的 size?减去自己的已用宽度或高度, 就是自己可以给子 View?size;至于 mode,就用 EXACTLY(注意:就算自己的 mode?AT_MOST,传给子 View?的也是EXACTLY,想不通的话好好琢磨一下);

自我测量,那么同样由于自己的可用空间和自己的两个 MeasureSpec?关,所以也需要根据自己的 widthMeasureSpec?heightMeasureSpec 中的 mode?来分情况判断:

  1. 如果自己的 spec?中的 mode?EXACTLY?或者 AT_MOST,说明自己的尺寸有上限,那么把 spec?中的 size?减去自己的已用宽度或高度, 就是自己可以给子 View?的尺寸上限;至于 mode,就用AT_MOST注意,就算自己的 mode?EXACTLY,传给子 View?的也AT_MOST,想不通的话好好琢磨一下;
  2. 如果自己的 spec?中的 mode?UNSPECIFIED,说明自己的尺寸没有上限,那么也就不必限制子 View?的上限,因此给子 View?限制的mode?就设置为 UNSPECIFIEDsize?0?就好。

测量出所有子 View?的位置和尺寸后,计算出自己的尺寸,并用setMeasuredDimension(width, height) 保存?

最后重写 onLayout()遍历每个子 View,调用它们的 layout() 方法来将位置和尺寸传给它们:完整代码

public class TagLayout extends ViewGroup {
    List<Rect> childrenBounds = new ArrayList<>();

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthUsed = 0;
        int heightUsed = 0;
        int lineWidthUsed = 0;
        int lineMaxHeight = 0;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specWidth = MeasureSpec.getSize(widthMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            if (specMode != MeasureSpec.UNSPECIFIED &&
                    lineWidthUsed + child.getMeasuredWidth() > specWidth) {
                lineWidthUsed = 0;
                heightUsed += lineMaxHeight;
                lineMaxHeight = 0;
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
            }
            Rect childBound;
            if (childrenBounds.size() <= i) {
                childBound = new Rect();
                childrenBounds.add(childBound);
            } else {
                childBound = childrenBounds.get(i);
            }
            childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());
            lineWidthUsed += child.getMeasuredWidth();
            widthUsed = Math.max(widthUsed, lineWidthUsed);
            lineMaxHeight = Math.max(lineMaxHeight, child.getMeasuredHeight());
        }

        int width = widthUsed;
        int height = heightUsed + lineMaxHeight;
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            Rect childBounds = childrenBounds.get(i);
            child.layout(childBounds.left, childBounds.top, childBounds.right, childBounds.bottom);
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
}

触摸反馈基础

自定义单 View 的触摸反馈

重写 onTouchEvent(),在方法内部定制触摸反馈算法

(1)是否消费事件取决于 ACTION_DOWN 事件是否返回 true

(2)MotionEvent

2.1 getActionMasked() getAction()

2.2 POINTER_DOWN / POINTER_UP getActionIndex()

? ? ? ?View.onTouchEvent()

(1)当用户按下(ACTION_DOWN):

如果不在滑动控件中,切换至按下状态,并注册长按计时? ; 如果在滑动控件中,切换至预按下状态,并注册按下计时?

(2)当进入按下状态并移动(ACTION_MOVE):

重绘 Ripple E?ect;? 如果移动出自己的范围,自我标记本次事件失效,忽略后续事件

(3)当用户抬起(ACTION_UP):

如果是按下状态并且未触发长按,切换至抬起状态并触发点击事件,并清除一切状态 ;如果已经触发长按,切换至抬起状态并清除一切状态

当事件意外结束(ACTION_CANCEL): 切换至抬起状态,并清除一切状态

自定义 ViewGroup 的触摸反馈

(1)除 了 重 写 onTouchEvent() , 还 需 要 重 写 onInterceptTouchEvent() (2)onInterceptTouchEvent() 不用在第一时间返回 true,而是在任意事件,需要拦截的时候返回true 就行

触摸反馈的流程

?

View.dispatchTouchEvent()

1.如果设置了 OnTouchListener,调用 OnTouchListener.onTouch()

(1)如果 OnTouchListener 消费了事件,返回 true

(2)如果 OnTouchListener 没有消费事件,继续调用自己的 onTouchEvent(),并返回和onTouchEvent() 相同的结果

2.如果没有设置 OnTouchListener,同上

ViewGroup.dispatchTouchEvent()

1.如果是用户初次按下(ACTION_DOWN),清空 TouchTargets DISALLOW_INTERCEPT ?标记拦截处理

2.如果不拦截并且不是 CANCEL 事件,并且是 DOWN 或者 POINTER_DOWN,尝试把pointer(手指)通过 TouchTarget 分配给子 View;并且如果分配给了新的子 View,调用child.dispatchTouchEvent() 把事件传给子 View

3.看有没有 TouchTarget

(1)如果没有,调用自己的 super.dispatchTouchEvent()

(2)如果有,调用 child.dispatchTouchEvent()?把事件传给对应的子 View(如果有的话)

4.如果是 POINTER_UP,从 TouchTargets?中清除 POINTER?信息,如果是 UP??CANCEL,重置状态

TouchTarget

作用:记录每个子 View 是被哪些 pointer(手指)

按下的结构:单向链表

拦截处理

1.如果不是初次按下,并且没有 TouchTarget,直接拦截

2.如果是初次按下,或者有 TouchTarget

(1)如果设置了 disallow intercept,不拦截

(2)否则,调用 onInterceptTouchEvent(),如果返回 true 则拦截,返回 false 则不拦截

?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-01 12:02:26  更:2021-09-01 12:04:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年10日历 -2024/10/23 5:50:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码