Android常规UI | 青训营笔记

168 阅读9分钟

这是我参与「第四届青训营 」笔记创作活动的第3天

Android常规UI | 青训营笔记

课程驱动:实现Activity中的UI。

课程要点

  1. UI组件
  2. 布局
  3. 渲染
  4. 交互
  5. 动画

UI的定义

UI:User Interface,用户图形界面,由多个不同功能的UI组件构成。Android SKD提供了大量的UI组件。

UI组件

认识常用组件和属性、方法

基础组件

组件Java ClassPackage
文本组件TextViewandroid.widget.TextView
图片组件ImageViewandroid.widget.ImageView
按钮组件Buttonandroid.widget.Button
输入框组件EditTextandroid.widget.EditText
复选框组件CheckBoxandroid.widget.CheckBox
单选按钮RadioButtonandroid.widget.RadioButton
.........

通用属性

  • id
  • height
  • width
  • margin
  • padding
  • ...

通用方法

  • setId(@IdRes int id)
  • setLayoutParams(ViewGroup, LayoutParams params)
  • setPadding(int left, int top, int right, int bottom)

注:①Android UI 的坐标原点为左上角。

属性选讲

属性描述
gravity子元素在该容器内的对齐方式
layout_gravity组件相对于父容器对齐方式

高级组件

组件Java Class
滑动ScrollView
列表组件ListView/RecyclerView
下拉刷新组件PullToRefresh
分页组件ViewPager
布局组件LinearLayout/RealtiveLayout/...
......

注:常规UI组件多是View,只能单独存在,只完成一项功能;高级UI组件大多是ViewGroup,可以嵌套UI组件。ViewGroup有更多功能,能包含View。

UI组件的关系

View和ViewGroup的关系 部分View的继承关系

布局

多个UI组件排列组合构成页面,这就是布局。布局包含大小、位置、层级。

LinearLayout

LinearLayout特定属性

属性功能描述
android:orientation布局内组件的排列方向
android:layout_weight布局内组件大小权重
android:divider布局内组件间分割线
android:showDiveders布局内组件间分割线位置
android:dividerPadding布局内分割线padding

注:①权重用于按比例划分屏幕。②LinearLayout内部还可以嵌套LinearLayout。

RelativeLayout

RelativeLayout特定属性
类别属性功能描述
根据父容器定位android:layout_centerInParent组件位于父容器中央位置
android:layout_centerHorizontal组件位于父容器水平中央位置
android:layout_centerVertical组件位于父容器垂直中央位置
android:layout_alignParentTop组件与父容器顶部对齐
android:layout_alignParentLeft组件与父容器左部对齐
android:layout_alignParentRight组件与父容器右部对齐
android:layout_alignParentBottom组件与父容器底部对齐
根据兄弟组件定位android:layout_above组件位于某组件上部
android:layout_below组件位于某组件下部
android:layout_toLeftOf组件位于某组件左部
android:layout_toRightOf组件位于某组件右部
android:layout_alignTop组件与某组件顶部对齐
android:layout_alignLeft组件与某组件左部对齐
android:layout_alignRight组件与某组件右部对齐
android:layout_alignBottom组件与某组件底部对齐

FrameLayout

FrameLayout特定属性
属性功能描述
android:foreground设置前景图像
android:foregroundGravity设置前景图像Gravity

前景图像:永远处于FrameLayout最上层,不会被覆盖的图片。

ConstraintLayout

ConstraintLayout特有属性

属性功能描述
layout_constraintLeft_toLeftOf组件左部与某组件左部对齐
layout_constraintLeft_toRightOf组件左部与某组件右部对齐
layout_constraintRight_toLeftOf组件右部与某组件左部对齐
layout_constraintRight_toRightOf...
layout_constraintTop_toTopOf...
layout_constraintTop_toBottomOf...
layout_constraintBottom_toBottomOf...
layout_constraintBottom_toTopOf...
layout_constraintBaseline_toBaselineOf组件基线与某组件基线2对齐
layout_constraintStart_toEndOf组件首部与某组件尾部对齐
layout_constraintStart_toStartOf...
layout_constraintEnd_toStartOf...
layout_constraintEnd_toEndOf...

渲染

要点

  • 布局加载
  • 布局解析
  • UI渲染

Android的UI组件一般由Java代码提供

布局加载

  1. 编写布局文件
  2. 注册Manifest
  3. 设置布局文件

首先在Activity的Java文件中设置布局文件

TextView textView;

@Override
public void onCreate(Bundle saveInstanceState) {}
    // call the super class onCreate to complete the creation o activity
    // like the view hierarchy
    super.onCreate(saveInstanceState);
    
    // set the UI layout for this activity
    setContentView(R.layout.main_activity)
    // setContentView最终创建了DecorView
    // 并由LayoutInflater来加载XML文件

    textView = (TextView)findViewById(R.id.text_view)
}

布局解析

LayoutInflater解析了XML文件,根据XML文件生成View实例,并将View实例添加到其ViewGroup中。

布局解析.JPG

布局渲染

为什么Activity在onResume()之后才显示?
onCreate(): setContentView()创建了DecorView,并将layout中的View添加至DecorView中

onResume(): ActivityThread.handleResumeActivity()
①WindowManagerImpl.addView ②创建ViewRootImpl ③ViewRootImpl.setView() ④ViewRootImpl.requestLayout(), 触发页面绘制

页面层次 页面层次.png

页面绘制流程 页面绘制流程

Vsync信号VSync信号.png

UI渲染流程 UI渲染流程

View绘制流程 View绘制流程

渲染流程 渲染流程

交互

常用交互事件监听器

回调方法事件监听器
onClick()View.OnClickListener,当用户轻触项目,或者使用导航键或轨迹球聚焦于项目,然后按适用于Enter键或按下轨迹球时,系统会调用此方法。
onLongClick()View.OnLongClickListener,当用户轻触并按住项目时,或者使用导航键或轨迹球聚焦于项目,然后按住适用的Enter键或按住轨迹球时,系统会调用此方法。
onFocusChange()View.OnFocusChangeListener,当用户使用导航键或轨迹球转到或离开项目时,系统会调用此方法。
onKey()View.OnFocusChangeListener,当用户聚焦于项目并按下或释放设备上的硬件按键时,系统会调用此方法。
onTouch()View.OnTouchListener,当用户执行可视为触摸事件的操作时,包括按下、释放或屏幕上任何移动手势,系统会调用此发方法。

触摸事件

当用户触摸屏幕时,系统将建立一系列的MotionEvent对象,MotionEvent包含发生触摸的位置和时间等细节信息。MotionEvent对象被传递到相应的捕获函数中。

事件类型常量含义说明
ACTION_DOWN
  • 手指接触屏幕时产生,表示一个触屏事件序列的开始
  • 在多点触摸时,只有第一个手指接触屏幕时才会产生此事件
ACTION_UP
  • 手指离开屏幕时产生,表示一个触屏事件序列的结束
  • 在多点触摸时,只有最后一个手指离开屏幕时才会产生此事件
ACTION_MOVE
  • 手指在屏幕上滑动时产生
  • 在多点触摸时,每一个手指的滑动都能产生
ACTION_CANCEL
  • 一个事件序列需要提早终止时由系统自动产生
ACTION_POINTER_DOWN
  • 只有在多点触摸时才会产生此事件
  • 在一个触屏事件序列中,除第一个接触屏幕的手指外,其余手指接触屏幕时会产生此事件
ACTION_POINTER_UP
  • 只有在多点触摸时才会产生此事件
  • 在一个触屏事件序列中,除最后一个离开屏幕的手指外,其余手指离开屏幕时会产生此事件

捕获触摸事件

Activity和View都有onTouchEvent(),用于处理触摸事件。 当用户触摸屏幕时,会回调触摸视图上的onTouchEvent()。对于最终被识别为手势的每个轻触事件序列,onTouchEvent() 都会多次被触发。

Touch事件相关方法功能描述ActivityViewGroupView
boolean dispatchTouchEvent(MotionEvent)事件分发YesYesYes
boolean onTnterceptTouchEvent(MotionEvent ev事件拦截NoYesNo
boolean onTouchEvent(MotionEvent ev)事件相应YesYesYes

事件处理流程

事件处理流程

动画

帧动画

帧动画

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/clock_12" android:duration="800"/>
    <item android:drawable="@drawable/clock_3" android:duration="800"/>
    <item android:drawable="@drawable/clock_6" android:duration="800"/>
    <item android:drawable="@drawable/clock_9" android:duration="800"/>
</animation-list>
ImageView mImageView  = (ImageView) findViewById(R.id.main_clock);
btn.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v) {
        mImageView.setImageResource(R.drawable.clock);
        AnimationDrawable animationDrawable = (AnimationDrawable) mImageView.getDrawable();
        animationDrawable.start();
    }
})

补间动画

补间动画

java类xml id值描述
AccelerateDecelerateInterpolator@android:anim/accelerate_decelerate_interpolator动画始末速率较慢,中间加速
AccelerateInterpolator@android:anim/accelerate_interpolator动画开始后逐渐加速
AnticipateInterpolator@android:anim/anticipate_interpolator开始时从后向前甩
AniticipateOvershootInterpolator@android:anim/anticipate_overshoot_interpolator类似上面AnticipateInterpolator
BounceInterpolator@android:anim/bounce_interpolator动画结束时弹起
CycleInterpolator@android:anim/cycle_interpolator循环播放速率改变为正弦曲线
DecelerateInterpolator@android:anim/decelerate_interpolator动画开始后逐渐减速
LinearInterpolator@android:anim/linear_interpolator动画匀速改变
OvershootInterpolator@android:anim/overshoot_interpolator向前弹出一定值之后回来到原来位置
PathInterpolator定义路径坐标后按照路径坐标来跑

属性动画

属性动画

public class AnimatorActivity extends AppCompatActivity {
    @Override
    protected void oCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_animator);
        reset();

        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                animatorSet.start();
            }
        });

    }

    protected void reset() {
        btn_start = findViewById(R.id.animator_start);
        mImageView = findViewById(R.id.animator_image);
        duration = 3000;

        Animator scaleXAnimator = ObjectAnimator.ofFloat(mImageView, "scaleX", 1, 0.5f);
        scaleXAnimator.setDuration(duration);
        Animator scaleYAnimator = ObjectAnimator.ofFloat(mImageView, "scaleY", 1, 0.5f);
        scaleYAnimator.setDuration(duration);
        Animator rotationXAnimator = ObjectAnimator.ofFloat(mImageView, "rotationX", 0, 360);
        rotationXAnimator.setDuration(duration);
        Animator rotationYAnimator = ObjectAnimator.ofFloat(mImageView, "rotationY", 0, 360);
        rotationYAnimator.setDuration(duration);
        animatorSet.play(scaleXAnimator).with(scaleYAnimator).before(rotationXAnimator).after(rotationYAnimator);
    }

    private long duration;
    private Button btn_start;
    private ImageView mImageView;
    private AnimatorSet animatorSet = new AnimatorSet();
}

动画总结

动画总结

两类动画的区别:是否改变动画本身的属性

  • 视图动画:不改变动画的属性,在动画过程中仅变换图像;
  • 属性动画:改变了动画的属性,在动画过程中改变了对象属性。

自定义View——以开关按钮为例

  1. 创建View
  2. 处理View布局
  3. 绘制View
  4. 处理用户交互
  5. 处理动画

自定义View

创建View

要求4个构造方法

构造器应用场景
1个参数Java代码中创建View
2个参数通过XML声明创建View
3个参数通过XML声明创建View+Style
4个参数通过XML声明创建View+Style+Theme
public class SwitchButton extends View implements Checkable {
    public SwitchButton(Context context) {
        super(context);
        init(context, null);
    }

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

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public SwitchButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }
}

处理View布局

复写方法应用场景
onMeasure一般需要复宽高设置为wrap_content场景、以及组件宽高有比例限制
onLayout继承自ViewGroup时必须复写,继承自View时一般不用
onSizeChanged视图大小发生改变时调用
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    if(widthMode == MeasureSpec.UNSPECIFIED
            || widthMode == MeasureSpec.AT_MOST){
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_WIDTH, MeasureSpec.EXACTLY);
    }
    if(heightMode == MeasureSpec.UNSPECIFIED
            || heightMode == MeasureSpec.AT_MOST){
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_HEIGHT, MeasureSpec.EXACTLY);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
    float viewPadding = Math.max(shadowRadius + shadowOffset, borderWidth);
    height = h - viewPadding - viewPadding;
    width = w - viewPadding - viewPadding;
    
    viewRadius = height * .5f;
    buttonRadius = viewRadius - borderWidth;

    left = top = viewPadding;
    right = w - viewPadding;
    bottom = h - viewPadding;

    centerX = (left + right) * .5f;
    centerY = (top + bottom) * .5f;

    buttonMinX = left + viewRadius;
    buttonMaxX = right - viewRadius;

    if(isChecked()){
        setCheckedViewState(viewState);
    }else{
        setUncheckViewState(viewState);
    }

    isUiInited = true;

    postInvalidate();
}

绘制View

核心类核心方法相应功能
PaintsetStyle设置绘制模式
setColor设置颜色
setStrokeWidth设置线条宽度
setTextSize设置文字大小
setAntiAlias设置抗锯齿开关
CanvasdrawCircle绘制圆形
drawRect绘制矩形
drawRoundRect绘制圆角矩形
drawBitmap绘制图片
drawLine绘制线条
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    paint.setStrokeWidth(borderWidth);
    paint.setStyle(Paint.Style.FILL);
    paint.setColor(background);
    drawRoundRect(canvas, left, top, right, bottom, viewRadius, paint);
    
    paint.setStyle(Paint.Style.STROKE);
    paint.setColor(uncheckCOlor);
    drawRoundRect(canvas, left, top, right, bottom, viewRadius, paint);
    
    ...
    
    drawButton(canvas, viewState, buttonX, centerY);
}

处理用户交互

交互事件操作逻辑
ACTION_DOWN设置操作状态
ACTION_MOVE处理拖拽,触发UI更新
ACTION_UP处理开关状态,播放动画
ACTION_CANCEL复位操作状态
@Override
public boolean onTouchEvent(MotionEvent event) {
    if(!isEnabled)
        return false;
    
    switch (actionMasked) {
        ...
        case MotionEvent.ACTION_MOVE:
            if (isPendingDragState()) {  // 在准备进入拖动状态过程中
                ...
            } else if (isDragState()) {  // 拖动按钮位置,同时改变对应的背景颜色
                ...
            }
            break;
        case MotionEvent.ACTION_UP:
            if (System.currentTimeMillis() - touchDownTime <= 300) {  // 点击
                toggle();
            } else if (isDragState()) {  // 在拖动状态,计算按钮位置,设置是否切换状态
                ...
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            removeCallbacks(postPendingDrag);
            break;
    }
    return true;
}

处理动画

  1. 设置属性动画,同时添加监听器
  2. 在动画监听器中,根据属性值跟新UI状态值,并触发UI绘制
// 初始化View时设置动画
valueAnimator = ValueAnimator.ofFloat(0f, 1f);
valueAnimator.setDuration(effectDuration);
valueAnimator.setRepeatCount(0);
valueAnimator.addUpdateListener(animatorUpdateListener);

// 点击开关后启动动画
valueAnimator.start();

// 简单动画更新回调,触发View绘制
private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float value = (Float)animation.getAnimatedValue();
        switch (animateState) {
            ...
            case ANIMATE_STATE_SWITCH:
                viewState.buttonX = beforeState.buttonX + (afterState.buttonX - beforeState.buttonX) * value;
                float fraction = (viewState.buttonX - buttonMinX) / (buttonMaxX - buttonMinX);
                viewState.checkStateColor = (int) argbEvaluator.evaluate( fraction,uncheckColor,checkedColor);
                viewState.radius = fraction * viewRadius; 
                viewState.checkedLineColor = (int) argbEvaluator.evaluate(fraction,Color.TRANSPARENT, checkLineColor);
                break;
        }
        postInvalidate();
    }
}