常规&高级UI编程(第三次课)|青训营笔记

79 阅读6分钟

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

Android UI组件

UI:UserInterface

图形用户界面

UI界面由多个不同格功能的UI组件构成

Android SDK中提供了大量的UI组件

常规UI组件大多由Android Framework中的Android.widget这个package提供。

常规UI组件

eec83c15ff00409e88bddbf39b6d19e0_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

常规UI组件的属性和方法

0d75a1008f6243acaafcfffe9b9672ba_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

常规UI组件的使用

IMG_0686.jpg

高级UI组件

IMG_0687.jpg

常规UI组件大多是View,高级UI组件大多是ViewGroup,比常规UI组件有更多的功能。

UI组件的关系

2cff23a71e3f43cc838b895c30329f61_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

acacb1da43204a6ab1e24961db02b113_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

布局

如何将多个UI组件组成一个页面:大小、、位置、层级

LinearLayout

IMG_0689.jpg

示例:

<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>

4478b890bfe9430bb119c6fc310e70d3_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

RelativeLayout

IMG_0690.jpg

示例:

<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>

a415d9b2e7ee415787804b6a9eefdc7c_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

FramgLayout

IMG_0691.jpg

示例:

<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>

8d6c06f2bd334348833412b7a19cf93a_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

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>

87e6d0abf1fb418a9c3921f2fe8b587d_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

总结

4e8e3616c18e44c084d54c7657081a32_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

渲染

布局加载

绘制一个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构造器。

布局解析-小结

IMG_0692.jpg

UI渲染

为什么Activity在onResume()之后才显示?

onCreate():setContentView()创建了DecorView,并将layout中的View添加进DecorView中。

onResume():ActivityThread.handleResumeActivity():
1.WindowManagerImpl.addView
2.创建ViewRootImpl
3.ViewRootImpl.setView
4.ViewRootImpl.requestLayout(),触发页面绘制

View绘制流程

IMG_0693.jpg

页面绘制流程

b00d3fb54405463d8295edc9a8b11b36_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

Vsync信号

IMG_0694.jpg

UI渲染流程

69f2f0ae5dd242b6b49930acd17e7c2c_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

渲染总结

2a876c9a2bbf48ff89b518a7435890a1_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

交互

为什么findViewByid可以找到对应的view实例?

IMG_0696.jpg

IMG_0697.jpg

常用事件监听器

e9b2137ed00d471ea1ca78340bb4e852_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

屏幕触摸事件

IMG_0699.jpg 所有的交互事件都来自于对屏幕触摸信号的处理,View.OnClickListener()等常用点击事件是对交互事件的二次封装

用户触摸屏幕时,系统将建立一系列的MotionEvent对象,MotionEvent包含关于发生触摸的位置和时间等细节信息,MotionEvent对象被传递到相应的捕获函数中,例如onTouchEvent() 58ac73b6efe549578182d90d7d0d447b_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

捕获触摸事件

  • 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);
        }
    }

触摸事件分发

IMG_0700.jpg

事件处理流程

db32eef138a24231b31fa37033737128_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

View的事件响应

1.在onToucheEvent()中的ACTION_DOWN设置了一个延时Runnable,用于处理onLongClickListener 2.在onTouchEvent()的ACTION_UP中,判断onLongClick是否执行,未执行则移除,然后执行onClickLlistener

交互总结

0f4cfaccbe3e4bea8957649edbbc4b99_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

动画

帧动画

3c6e38738fce4bc48586538239e4d093_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

示例

<?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(); 
}

8d85e81248d44a9bb1be133ec29e092a_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

补间动画

34ddbc4643dd4345a71fdd6aa5f7114e_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

示例:

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);
}

79d02c81ed024ece9f62cfb8584f792a_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

Interpolater:是一个接口,设置属性值从初始值过渡到结束值的变化规律。 043a94dbacfd45bd891185b0e5ce954b_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

50cb0205966f4835933fc3ce1944e584_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

View动画小结

IMG_0702.jpg

属性动画

040cce5482b84bd18eb9464fd92dc7ef_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

示例:

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(); 
}

f92396c167cd40bd99fa794f929cc7c0_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

动画总结

9c28389ad6374173b8f1c7d961502b8d_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

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

  • 视图动画:不改变动画的属性,在动画过程中仅对图像进行变换来达到动画效果。无论动画结果在哪,该View的位置和响应区域都是在原地,不会根据结果而移动;
  • 属性动画:改变了动画属性 因属性动画在动画过程中对动态改变了对象属性,从而达到了动画效果

自定义UI

自定义View示例

1.涉及UI组件:与第一节课契合
2.涉及布局
3.涉及渲染:与第二节课契合
4.涉及交互:与第四节课契合
5.涉及动画:与第五节课契合
6.涉及自定义View:第六节课内容

ba03826c59494f22b442fd47986ab528_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

创建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);
    }

IMG_0703.jpg 尽量四个构造器都写全,不要改变参数顺序。

处理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;
    ...
}

IMG_0704.jpg

绘制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);
}

IMG_0705.jpg

处理用户交互:

@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;
}

IMG_0706.jpg

处理动画:

// 初始化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绘制

自定义View小结

95034715f7bb4ffa9a6310b34465bb9c_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg

总结

f81a855f8ac84c4ab08052e148577ea3_tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.jpg