Activity

创建

  1. 创建XXActivity.java,继承AppCompatActivity
  2. 在AndroidManifest.xml中注册,写法形如<activity android:name="chapter.android.aweme.ss.com.chapter2.xmlparser.XmlActivity" />
  3. 编写布局xml,放在res/layout目录下
  4. 在XXActivity.java类中重写onCreate方法,并用setContentView(xml)展示相应的布局,形如setContentView(R.layout.activity_main);

跳转与关闭

每个Activity相当于一个页面,应用的页面之间可以进行跳转。使用startActivity()方法进行跳转。在跳转时,必须传给该方法一个Intent(意图)。意图至少要指明当前上下文和跳转的页面Activity类,也可以增加其他额外的数据。最简单的意图形如new Intent(this, XmlActivity.class),传给startActivity()后可以跳转到XmlActivity。

intent包含四个重要的内容:

  • Action 值为字符串,通常是系统中已经定义的常用行为
  • Data 通常是URI格式的操作数
  • Category 指定当前Action被执行的环境,默认为CATEGORY_DEFAULT
  • Extras 一些补充的信息传递,通常用Bundle进行封装后传递

在定义intent的时候可以对这四个信息进行set,而跳转的目标界面也可以拿到随之传来的这四个内容,提供一定的服务。

此外,intent还有显式和隐式之分:

  • 显式intent明确指定了要跳转到哪一个Activity
  • 隐式intent不明确指明,而是填好action、catagory、data等信息,然后交给系统,系统去寻找匹配这个intent的所有界面(或者应用)。一个典型的例子就是应用唤出浏览器,就可以通过隐式intent预埋的方式实现。

如果要通过隐式intent跳转到另一个Activity,则必须在AndroidManifest.xml文件中给目标Activity注册一个intent-filter,并定义好这个Activity匹配的action和category,方便系统进行匹配。

隐式的intent往往可以在布局文件中直接绑定到Button上,例如:

    <Button
        android:id="@+id/btn_implicit_preview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="openBrowser"
        android:text="隐式intent预埋(打开浏览器)" />
    /**
     * 隐式intent预埋
     *
     * @param view
     */
    public void openBrowser(View view) {
        Intent it = new Intent();
        it.setAction(Intent.ACTION_VIEW);
        it.setData(Uri.parse("http://www.baidu.com"));
        startActivity(it);
    }

关闭时,调用finish()退出Activity。在真正使用时,返回键退出当前页面的时候也会调用这个方法。可以对该方法进行重写。

生命周期

Activity

可以在XXActivity.java中重写这7个方法来在适当的实际完成一些额外的动作。

退出重进或者被系统杀掉重启都会导致Activity对象被重新构建,重新初始化,之前的数据会丢失。有一个方法可以让之前的数据能够在重新构建时被加入,看起来就像Activity的状态被保存了下来。重写onSaveInstanceState()和onRestoreInstanceState()方法:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        logAndAppend(ON_SAVE_INSTANCE_STATE);
        String content = mLifecycleDisplay.getText().toString();//当前已有的log 提取出来
        outState.putString(LIFECYCLE_CALLBACKS_TEXT_KEY, content); //把内容存储起来
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        logAndAppend(ON_RESTORE_INSTANCE_STATE);
    }

onSaveInstanceState方法在Destroy之前执行,把当前的状态进行保存。然后下次重新创建之前会先调用onRestoreInstanceState方法把数据恢复成之前的模样。

基础UI组件

UI组件分为两类:View组件单元和ViewGroup组织组件的单元。两者的关系类似树中的叶节点和连接叶节点的父节点一样。

View

  • TextView 文本框
  • EditText 输入框
  • Button 按钮
  • ImageView 图片

需要注意的是,不仅仅按钮能够通过点击和用户交互,文本框、图片等很多组件也可以设置点击的监听器,对用户点击的动作做出反应。

ViewGroup

  • LinearLayout 线性布局。可以设置为水平或者垂直的。
  • RelativeLayout 相对布局。其中每个元素的位置可以指定为相对于其他元素的定位。
  • FrameLayout 帧布局。每个组件之间可以相互覆盖的布局。

下图表示了诸多View和ViewGroup的继承关系。

View

一个布局文件的最外层应当是一个ViewGroup。

屏幕适配

wrap_content、match_parent、weight、gravity这些关键字的属性可以将组件的大小、位置自动适应内容或者外层布局。

minWidth、minHeight 可以约束一个组件的最小尺寸。

Nine-Patch点九图可以依据内容自动拉伸,而且不影响视觉效果。

事件传递

触摸事件对应MotionEvent类,三种事件类型:

  • ACTION_DOWN,手指触碰屏幕时发生的事件
  • ACTION_MOVE,手指在屏幕上滑动时发生的事件
  • ACTION_UP,手指离开屏幕时发生的事件

这三种事件发生时,第一个接收到事件的会是当前的Activity,然后是页面布局上最外层的ViewGroup,接着是下层的ViewGroup或者View,层层嵌套。每一层都可以自己决定当有事件传来时如何处理这个事件。

处理事件有三个动作:

  • dispatch分发,用dispatchTouchEvent(MotionEvent event)重写
  • intercept拦截,用onInterceptTouchEvent(MotionEvent event)重写
  • consume消费,用onTouchEvent(MotionEvent event)重写

首先要决定如何分发这个事件,然后分发给下层的过程中可以拦截,如果不分发给下层则会自己消费掉。下图即为正常情况下的事件分发逻辑:

event

消费即意味着事件传递的终点。super表示类似return super.dispatchTouchEvent(ev);这样的情况。ViewGroup的super默认都是返回false的。

也可以参考下面这张图:

TouchEvent

高级UI组件之列表

ListView

是一个显示可滚动项目的视图组件。需要配合使用Adapter将内容插入列表中。Adapter负责将每一个列表元素的视图样式和数据结合起来,填入ListView中去。

ListView

ListView每次会去找Adapter请求需要的View,Adapter内部将数据和item的布局结合后返回给ListView这个Item的View,这样ListView就可以显示每一个Item了。

ListView本身具有缓存复用的机制,可以防止一次加载过多的item导致卡顿。每次ListView只会加载稍多于一个屏幕能够显示的item数量,滑动屏幕时会将移出屏幕的item回收并加入新出现的item,大大提高了加载效率。不过这些都是ListView内部实现,我们使用者不用太在意。

重点是Adapter。有四种Adapter可供我们使用:

  • ArrayAdapter:简单、易用的 Adapter,用于将数组数据作为数据源绑定到列表项中。支持泛型操作
  • SimpleAdapter:相比 ArrayAdapter 来说,功能比较强大,可以将数据源的数据一一的绑定到 item 中的 view 中。
  • CursorAdapter:用于绑定游标(直接从数据库取出数据)作为列表项的数据源,和数据库有关系,不常用。
  • BaseAdapter:这个是我们在实际开发中经常用到的,我们需要继承 BaseAdapter 来自定义我们自己的适配器

其中ArrayAdapter和SimpleAdapter都直接使用就可,一般不需要重新定义。但是如果这两种Adapter不能满足使用的要求,需要继承BaseAdapter类来实现自己的Adapter。必须重定义的方法有四个:

// 继承 BaseAdapter 必须要实现它的 4 个方法
class MyAdapter extends BaseAdapter{
    
        // 返回适配器中所代表的数据集合的条数
        // 会首先执行这个方法(连续执行好几次),如果是 0 则后面的方法就不会执行了
        @Override
        public int getCount() {
            return 0;
        }
        
        // 返回数据集合中指定索引 position 对应的数据项
        // 手动调用才会执行
        @Override
        public Object getItem(int position) {
            return null;
        }
        // 返回列表中与指定索引对应的行 id
        // 手动调用才会执行
        @Override
        public long getItemId(int position) {
            return 0;
        }
        // 返回指定索引对应的数据的视图,会多次调用
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return null;
        }
}

最核心的方法还是getView方法,因为ListView正是通过这个方法拿到每一个item的。观察这个方法中有一个convertView,如果ListView调用时传入的convertView不为null,说明ListView腾出了一个itemview的地方可以供我们复用,这个时候就要充分利用复用的机制来提高效率了,不需要再重新new一个view,而是可以直接拿这个convertView修改一下里面的数据重新传回去即可。最佳的实践方法是借助ViewHolder进行复用:

class ViewHolder{
    TextView tv;
    ImageView iv;
    Button bt;
}
@Override
public View getView(int position,View convertView,ViewGroup parent){
    ViewHolder viewHolder;
    if(convertView == null){
        convertView = mInflater.inflater(R.layout.item,null);
        viewHolder = new ViewHolder();
        viewHolder.tv = convertView.findViewById(R.id.tv);
        viewHolder.iv = convertView.findViewById(R.id.iv);
        viewHolder.bt = convertView.findViewById(R.id.bt);
        // 将 viewHolder 绑定到 convertView 中实现复用
        convertView.setTag(viewHolder);
    }else{// 如果convertView不为空说明可以复用,简单修改数据即可,无需重新创建视图
            viewHolder = (ViewHolder)convertView.getTag();
        }
        viewHolder.iv.set.....
        .....各种设置
        return convertView;
}

另外一个小tips就是不要在滑动的时候加载图片,否则可能会卡顿。

这部分内容参考博客ListView详细介绍与使用,里面介绍十分详细有用,很有参考价值。

RecycleView

android support-v7包 中新增的一个ListView升级版控件,对列表展示进行了更好的优化处理。

首先,RecycleView可以进行列表排列调整,只需要在Activity中setLayoutManager即可,参考如下:

mNumbersListView = findViewById(R.id.rv_numbers);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mNumbersListView.setLayoutManager(layoutManager);

// 或者可以写成
mRecyclerView.setLayoutManager(new
        StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.VERTICAL));// 流式布局

mRecycleView.setLayoutManager(new LinearLayoutManager(getContext()));// 普通的按列排列

然后重点就是RecycleView的Adapter写法。先上一个例子:

public class GreenAdapter extends RecyclerView.Adapter<GreenAdapter.NumberViewHolder> {

    private static final String TAG = "GreenAdapter";
    private int mNumberItems;
    private final ListItemClickListener mOnClickListener;
    private static int viewHolderCount;

    public GreenAdapter(int numListItems, ListItemClickListener listener) {
        mNumberItems = numListItems;
        mOnClickListener = listener;
        viewHolderCount = 0;
    }

    // 必须重定义的方法onCreateViewHolder,核心方法
    // 作用:在需要的时候创建新的ViewHolder。通常发生在列表刚开始展示那会儿,缓存里没有Holder,需要创建。下拉列表的时候,也可能缓存中Holder不够用,需要补充Holder
    //  注意:这个方法仅仅负责创建,不负责填入Holder中View内的数据
    @NonNull
    @Override
    public NumberViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
        // 创建
        Context context = viewGroup.getContext();
        int layoutIdForListItem = R.layout.number_list_item;
        LayoutInflater inflater = LayoutInflater.from(context);
        boolean shouldAttachToParentImmediately = false;
        View view = inflater.inflate(layoutIdForListItem, viewGroup, shouldAttachToParentImmediately);
        NumberViewHolder viewHolder = new NumberViewHolder(view);

        // 初始化
        viewHolder.viewHolderIndex.setText("ViewHolder index: " + viewHolderCount);
        int backgroundColorForViewHolder = ColorUtils
                .getViewHolderBackgroundColorFromInstance(context, viewHolderCount);
        viewHolder.itemView.setBackgroundColor(backgroundColorForViewHolder);

        Log.d(TAG, "onCreateViewHolder: number of ViewHolders created: " + viewHolderCount);
        viewHolderCount++;
        return viewHolder;
    }

    // 必须重定义的核心方法,负责将缓存中的Holder绑定新数据,从而实现复用。
    @Override
    public void onBindViewHolder(@NonNull NumberViewHolder numberViewHolder, int position) {
        Log.d(TAG, "onBindViewHolder: #" + position);
        numberViewHolder.bind(position);
    }

    // 必须重定义
    @Override
    public int getItemCount() {
        return mNumberItems;
    }

    // 必须定义Holder,Google将ListView中的好习惯直接规范化,核心组件
    public class NumberViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        
        private final TextView viewHolderIndex;
        private final TextView listItemNumberView;

        // 构造时,和一个视图绑定起来,以后只会更换视图内的数据展示。
        public NumberViewHolder(@NonNull View itemView) {
            super(itemView);
            listItemNumberView = (TextView) itemView.findViewById(R.id.tv_item_number);
            viewHolderIndex = (TextView) itemView.findViewById(R.id.tv_view_holder_instance);
            itemView.setOnClickListener(this); // 通过这种方法将点击事件传出来
        }

        // 负责更新与之绑定的视图中的数据,这里可以在onBindViewHolder中调用
        public void bind(int position) {
            listItemNumberView.setText(String.valueOf(position));
        }

        // 将点击事件传给外面的Activity。这里的监听者一般是外面的Activity
        @Override
        public void onClick(View v) {
            int clickedPosition = getAdapterPosition();
            if (mOnClickListener != null) {
                mOnClickListener.onListItemClick(clickedPosition);
            }
        }
    }

    // 如果外面的Activity想要监听列表元素的点击事件,就要实现这个接口
    public interface ListItemClickListener {
        void onListItemClick(int clickedItemIndex);
    }
}

总结起来,至少要自己实现三个方法和一个内部类:

  • 核心方法onCreateViewHolder,需要时创建Holder,系统会调用
  • 核心方法onBindViewHolder,更换Holder绑定的View内部的数据展示,系统自动调用
  • getItemCount,系统调用或者自己调用得到列表数量总数
  • 核心组件Holder,必须继承RecyclerView.ViewHolder

如果需要监听点击事件,还需要一个接口和在Holder中帮助View传递给监听者(Activity)。

另外,RecycleView还可以借助ItemAnimator实现一些小动画,更加好看。

在内部的实现中,RecycleView采用的是三级缓存,比ListView更加灵活,效率更高,也支持局部刷新。因此应尽量使用RecycleView来展示列表。

本节示例工程

https://github.com/jingjiecb/Chapter-2