这是我参与「第四届青训营 」笔记创作活动的第3天
Android常规UI | 青训营笔记
课程驱动:实现Activity中的UI。
课程要点
- UI组件
- 布局
- 渲染
- 交互
- 动画
UI的定义
UI:User Interface,用户图形界面,由多个不同功能的UI组件构成。Android SKD提供了大量的UI组件。
UI组件
认识常用组件和属性、方法
基础组件
| 组件 | Java Class | Package |
|---|---|---|
| 文本组件 | TextView | android.widget.TextView |
| 图片组件 | ImageView | android.widget.ImageView |
| 按钮组件 | Button | android.widget.Button |
| 输入框组件 | EditText | android.widget.EditText |
| 复选框组件 | CheckBox | android.widget.CheckBox |
| 单选按钮 | RadioButton | android.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组件的关系
布局
多个UI组件排列组合构成页面,这就是布局。布局包含大小、位置、层级。
LinearLayout
LinearLayout特定属性
| 属性 | 功能描述 |
|---|---|
| android:orientation | 布局内组件的排列方向 |
| android:layout_weight | 布局内组件大小权重 |
| android:divider | 布局内组件间分割线 |
| android:showDiveders | 布局内组件间分割线位置 |
| android:dividerPadding | 布局内分割线padding |
注:①权重用于按比例划分屏幕。②LinearLayout内部还可以嵌套LinearLayout。
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
| 属性 | 功能描述 |
|---|---|
| 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代码提供
布局加载
- 编写布局文件
- 注册Manifest
- 设置布局文件
首先在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中。
布局渲染
为什么Activity在onResume()之后才显示?
onCreate(): setContentView()创建了DecorView,并将layout中的View添加至DecorView中
onResume(): ActivityThread.handleResumeActivity()
①WindowManagerImpl.addView
②创建ViewRootImpl
③ViewRootImpl.setView()
④ViewRootImpl.requestLayout(), 触发页面绘制
页面层次
页面绘制流程
Vsync信号
UI渲染流程
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事件相关方法 | 功能描述 | Activity | ViewGroup | View |
|---|---|---|---|---|
| boolean dispatchTouchEvent(MotionEvent) | 事件分发 | Yes | Yes | Yes |
| boolean onTnterceptTouchEvent(MotionEvent ev | 事件拦截 | No | Yes | No |
| boolean onTouchEvent(MotionEvent ev) | 事件相应 | Yes | Yes | Yes |
事件处理流程
动画
帧动画
<?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——以开关按钮为例
- 创建View
- 处理View布局
- 绘制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
| 核心类 | 核心方法 | 相应功能 |
|---|---|---|
| Paint | setStyle | 设置绘制模式 |
| setColor | 设置颜色 | |
| setStrokeWidth | 设置线条宽度 | |
| setTextSize | 设置文字大小 | |
| setAntiAlias | 设置抗锯齿开关 | |
| Canvas | drawCircle | 绘制圆形 |
| 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;
}
处理动画
- 设置属性动画,同时添加监听器
- 在动画监听器中,根据属性值跟新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();
}
}