这是我参与「第四届青训营」笔记创作活动的第5天。
Android UI组件
UI:UserInterface
图形用户界面
UI界面由多个不同格功能的UI组件构成
Android SDK中提供了大量的UI组件
常规UI组件大多由Android Framework中的Android.widget这个package提供。
常规UI组件
常规UI组件的属性和方法
常规UI组件的使用
高级UI组件
常规UI组件大多是View,高级UI组件大多是ViewGroup,比常规UI组件有更多的功能。
UI组件的关系
布局
如何将多个UI组件组成一个页面:大小、、位置、层级
LinearLayout
示例:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:orientation="vertical" >
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top" />
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="right" />
</LinearLayout>
RelativeLayout
示例:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:paddingLeft="16dp" android:paddingRight="16dp" >
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Spinner
android:id="@+id/dates"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/times" />
<Spinner
android:id="@id/times"
android:layout_width="96dp" android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_alignParentRight="true" />
<Button
android:layout_width="96dp" android:layout_height="wrap_content"
android:layout_below="@id/times"
android:layout_alignParentRight="true" />
</RelativeLayout>
FramgLayout
示例:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<TextView
android:layout_width="300dp"
android:layout_height="300dp"
android:gravity="center"
android:background="@android:color/holo_blue_bright"
android:text="我是第一层"/>
<TextView
android:layout_width="150dp"
android:layout_height="140dp"
android:gravity="center"
android:background="@android:color/holo_green_light"
android:text="我是第二层"/>
</FrameLayout>
ConstraintLayout
示例:
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_beauty"
android:layout_width="100dp"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:src="@drawable/beauty"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_girl"
android:layout_width="100dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:src="@drawable/girl"
app:layout_constraintBottom_toBottomOf="@+id/iv_beauty"
app:layout_constraintLeft_toRightOf="@+id/iv_beauty"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
总结
渲染
布局加载
绘制一个Activity要进行三步:1.编写布局文件。2.注册Manifest。3.在ACtivity中设置布局文件。
XML里描述的View是怎么绘制到屏幕上的?通过向下查找源码,一直到AppCompatDelegate类里面的create()方法后接着找到setContentView(int resId)方法可知,setContentView最终创建了DecorView,并由LayoutInflater来加载了XML文件。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor
.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
布局解析
LayoutInflater到底做了什么?它有一个XML文件的解析器,往下继续进行解析,调用内部的rInflate(XmlPullParser attrs,boolean finishInflate)方法,根据XML文件生成了View实例,并将View实例添加到了其ViewGroup中。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
while (((type = parser.next()) != XmlPullParser.END_TAG||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 省略
// 核心代码
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
XML中的View是如何生成实例的?根据XML中View类名找到相应的View,并将XML中的描述属性解析为AttributeSet,并作为第二个参数传给了View构造器。
布局解析-小结
UI渲染
为什么Activity在onResume()之后才显示?
onCreate():setContentView()创建了DecorView,并将layout中的View添加进DecorView中。
onResume():ActivityThread.handleResumeActivity():
1.WindowManagerImpl.addView
2.创建ViewRootImpl
3.ViewRootImpl.setView
4.ViewRootImpl.requestLayout(),触发页面绘制
View绘制流程
页面绘制流程
Vsync信号
UI渲染流程
渲染总结
交互
为什么findViewByid可以找到对应的view实例?
常用事件监听器
屏幕触摸事件
所有的交互事件都来自于对屏幕触摸信号的处理,View.OnClickListener()等常用点击事件是对交互事件的二次封装
用户触摸屏幕时,系统将建立一系列的MotionEvent对象,MotionEvent包含关于发生触摸的位置和时间等细节信息,MotionEvent对象被传递到相应的捕获函数中,例如onTouchEvent()
捕获触摸事件
- Activity和View都有onTouchEvent(),用于处理触摸事件。
- 当用户触摸屏幕时,会回调触摸视图上的onTouchEvent()。 对于最终被识别为手势的每个轻触事件序列,onTouchEvent() 都会多次被触发。
public class MainActivity extends Activity {
@Override
public boolean onTouchEvent(MotionEvent event){
int action = MotionEventCompat.getActionMasked(event);
switch(action) {
case (MotionEvent.ACTION_DOWN) :
Log.d(DEBUG_TAG,"Action was DOWN");
return true;
case (MotionEvent.ACTION_MOVE) :
Log.d(DEBUG_TAG,"Action was MOVE");
return true;
case (MotionEvent.ACTION_UP) :
Log.d(DEBUG_TAG,"Action was UP");
return true;
case (MotionEvent.ACTION_CANCEL) :
Log.d(DEBUG_TAG,"Action was CANCEL");
return true;
case (MotionEvent.ACTION_OUTSIDE) :
Log.d(DEBUG_TAG,"Movement occurred outside bounds " + "of current screen element");
return true;
default :
return super.onTouchEvent(event);
}
}
触摸事件分发
事件处理流程
View的事件响应
1.在onToucheEvent()中的ACTION_DOWN设置了一个延时Runnable,用于处理onLongClickListener 2.在onTouchEvent()的ACTION_UP中,判断onLongClick是否执行,未执行则移除,然后执行onClickLlistener
交互总结
动画
帧动画
示例
<?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/ic_wifi_0" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_1" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_2" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_3" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_4" android:duration="100"/>
<item android:drawable="@drawable/ic_wifi_5" android:duration="100"/>
</animation-list>
private void playAnimation() {
mImageView.setImageResource(R.drawable.frame_anim);
AnimationDrawable animationDrawable = (AnimationDrawable) mImageView.getDrawable();
animationDrawable.start();
...
animationDrawable.stop();
}
补间动画
示例:
public void tweenedAnimation(View view) {
// 创建一个透明度动画,透明度从1渐变至0
AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
alphaAnimation.setDuration(3000);
// 创建一个旋转动画,从0度旋转至360度
RotateAnimation rotateAnimation = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(3000);
ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(3000);
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1,
Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1);
translateAnimation.setDuration(3000);
// 组合上述4种动画
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(alphaAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(translateAnimation);
view.startAnimation(animationSet);
}
Interpolater:是一个接口,设置属性值从初始值过渡到结束值的变化规律。
View动画小结
属性动画
示例:
private void startObjectAnimatorSet() {
// 创建一个ObjectAnimator,将mImageView的scaleX属性值从1变化到0.5
Animator scaleXAnimator = ObjectAnimator.ofFloat(mImageView, "scaleX", 1, 0.5f);
scaleXAnimator.setDuration(2000);
// 创建一个ObjectAnimator,将mImageView的scaleY属性值从1变化到0.5
Animator scaleYAnimator = ObjectAnimator.ofFloat(mImageView, "scaleY", 1, 0.5f);
scaleYAnimator.setDuration(2000);
// 创建一个ObjectAnimator,将mImageView的rotationX属性值从0变化到360
Animator rotationXAnimator = ObjectAnimator.ofFloat(mImageView, "rotationX", 0, 360);
rotationXAnimator.setDuration(2000);
// 创建一个ObjectAnimator,将mImageView的rotationY属性值从0变化到360
Animator rotationYAnimator = ObjectAnimator.ofFloat(mImageView, "rotationY", 0, 360);
rotationYAnimator.setDuration(2000);
// 组合上述4种动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(scaleXAnimator).with(scaleYAnimator)
.before(rotationXAnimator).after(rotationYAnimator);
animatorSet.start();
}
动画总结
两类动画的根本区别在于:是否改变动画本身的属性
- 视图动画:不改变动画的属性,在动画过程中仅对图像进行变换来达到动画效果。无论动画结果在哪,该View的位置和响应区域都是在原地,不会根据结果而移动;
- 属性动画:改变了动画属性 因属性动画在动画过程中对动态改变了对象属性,从而达到了动画效果
自定义UI
自定义View示例
1.涉及UI组件:与第一节课契合
2.涉及布局
3.涉及渲染:与第二节课契合
4.涉及交互:与第四节课契合
5.涉及动画:与第五节课契合
6.涉及自定义View:第六节课内容
创建View:
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布局
@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;
...
}
绘制View:
@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);
...
//绘制按钮左边绿色长条遮挡
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(1);
drawArc(canvas, left, top, left + 2 * viewRadius, top + 2 * viewRadius,90, 180, paint);
canvas.drawRect( left + viewRadius, top,viewState.buttonX,
top + 2 * viewRadius,paint);
...
//绘制按钮
drawButton(canvas, viewState.buttonX, centerY);
}
处理用户交互:
@Override
public boolean onTouchEvent(MotionEvent event) {
if(!isEnabled()) { return false; }
switch (actionMasked){
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_MOVE:
if(isPendingDragState()){ //在准备进入拖动状态过程中,可以拖动按钮位置
...
}else if(isDragState()){ //拖动按钮位置,同时改变对应的背景颜色
...
}
break;
case MotionEvent.ACTION_UP:
if(System.currentTimeMillis() - touchDownTime <= 300){ //点击时间小于300ms,认为是点击操作
toggle();
}else if(isDragState()){ //在拖动状态,计算按钮位置,设置是否切换状态
...
}
break;
case MotionEvent.ACTION_CANCEL:
removeCallbacks(postPendingDrag);
break;
}
return true;
}
处理动画:
// 初始化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();
}
};
1.设置属性动画,同时添加监听器
2.在动画监听器中,根据属性值更新UI状态值,并触发UI绘制