第三章 UI开发

3.2 常用的控件

3.2.1 TextView

  • android:gravity指文字的对齐方式:top/bottom/left/right/center
  • android:textSize="24sp" android中字体大小以sp为单位

3.2.2 Button

Button点击事件的注册模式有两种,一是以匿名类方式,二是以继承接口的方式.
匿名类方式示例如下:

public class FirstActivity extends BaseActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.first_layout);
        Button button1 = findViewById(R.id.button1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               // 业务逻辑
            }
        });
    }
}

继承接口方式示例:

public class ThirdActivity extends BaseActivity implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.third_layout);
        Button button = findViewById(R.id.button3);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.button3:
                ActivityCollector.finishAll();
                Log.d("ThirdActivity", String.format("Process exit %s", android.os.Process.myPid()));
                android.os.Process.killProcess(android.os.Process.myPid());
                break;
            default:
                break;
        }
    }
}

3.2.3 EditText

  • android:hint 可以给EditText一个提示性的文本
  • android:maxLines 指定了EditText的最大行数为两行,超过两行后文本自动向上滚动而不会在继续拉伸
    增加一个button来获取EditText输入的内容,button使用继承的方式来实现监听.新建项目UIWidgetTest,MainActivity代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editText;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(this);
        editText = findViewById(R.id.edit_text);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.button:
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();
                break;
            default:
                break;
        }

    }
}

3.2.4 ImageView

ImageView是一个展示图片的控件,图片一般是放在drawable开头的目录下.默认项目会创建一个空的drawable目录,不过由于这个目录没有指定具体的分辨率,所以一般不使用此目录来放置图片.在res下新建drawable-xhdpi目录,并将准备好的图片复制到目录中.在android视图下不显示新建的目录,可以切换到project视图下.在新建drawable-xhdpi目录时,需要将Available qualifiers属性选择为Density并选择对应的分辨率.
修改activity_main.xml来显示图片,因为未知图片的宽和高,所以设置为wrap_content,保证图片完全显示.代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Type something"
        android:maxLines="2" />
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="button"></Button>
    <ImageView
        android:id="@+id/image_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/img7"></ImageView>
</LinearLayout>

通过程序可以动态的修改ImageView中显示的图片,修改MainActivity代码后如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editText;
    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        editText = findViewById(R.id.edit_text);
        imageView = findViewById(R.id.image_view);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
                imageView.setImageResource(R.drawable.img8);
                break;
            default:
                break;
        }

    }
}

3.2.5 ProgressBar

progressBar用于显示进度条,基本用法如下:

<ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

默认情况下.进度是可见的.所有的Android控件的都有可见属性,通过android:visibility来指定,可选参数有:visible,invisible,gone.

  • visible :可见
  • invisable:不可见,但占位置
  • gnoe: 不可见,不占位置
    在MainActivity中增加一个Button,用来测试修改progressBar的状态.修改后的avtivity_main.xml如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/progress_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="change ProgressBar"/>
</LinearLayout>

修改后MainActivity.java如下


public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editText;
    private ImageView imageView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        editText = findViewById(R.id.edit_text);
        imageView = findViewById(R.id.image_view);
        button.setOnClickListener(this);

        progressBar = findViewById(R.id.progress_bar);
        Button processButton = findViewById(R.id.progress_button);
        processButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
                imageView.setImageResource(R.drawable.img8);
                break;
            case R.id.progress_button:
               if (progressBar.getVisibility() == View.GONE) {
                   progressBar.setVisibility(View.VISIBLE);
               } else {
                   progressBar.setVisibility(View.GONE);
               }
               break;
            default:
                break;
        }

    }
}

默认情况下ProgressBar是圆形的,可以修改其样式,比如改成水平进度条

 <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="?android:attr/progressBarStyleHorizontal"
        android:max="100"/>

style定义了样式,max属性给进度条设置了一个最大值.修改MainActivity.java,使每点击一次按钮,进度条进度前进一下:


public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editText;
    private ImageView imageView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        editText = findViewById(R.id.edit_text);
        imageView = findViewById(R.id.image_view);
        button.setOnClickListener(this);

        progressBar = findViewById(R.id.progress_bar);
        Button processButton = findViewById(R.id.progress_button);
        processButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
                imageView.setImageResource(R.drawable.img8);
                break;
            case R.id.progress_button:
//                修改可见性
//                if (progressBar.getVisibility() == View.GONE) {
//                    progressBar.setVisibility(View.VISIBLE);
//                } else {
//                    progressBar.setVisibility(View.GONE);
//                }
                int progress = progressBar.getProgress();
                progress = progress+10;
                progressBar.setProgress(progress);
                break;
            default:
                break;
        }

    }
}

3.2.6 AlertDialog

AlertDialog可以在当前的界面弹出一个对话框,这个对话框位置置于所有元素之上,能够屏蔽掉其他控件的交互能力.AlertDialog通过AlertDialog.Builder来实现.setPositiveButton()方法设置对话框确定按钮的事件,setNegativeButton()方法设置取消按钮的事件.修改MainActivity.java如下,点击更新进度的按钮后,弹出提示.dialog.setCancelable(false)表示不能通过Back键取消掉.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editText;
    private ImageView imageView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        editText = findViewById(R.id.edit_text);
        imageView = findViewById(R.id.image_view);
        button.setOnClickListener(this);
        progressBar = findViewById(R.id.progress_bar);
        Button processButton = findViewById(R.id.progress_button);
        processButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
                imageView.setImageResource(R.drawable.img8);
                break;
            case R.id.progress_button:
//                修改可见性
//                if (progressBar.getVisibility() == View.GONE) {
//                    progressBar.setVisibility(View.VISIBLE);
//                } else {
//                    progressBar.setVisibility(View.GONE);
//                }
//                测试进度
                int progress = progressBar.getProgress();
                progress = progress+10;
                progressBar.setProgress(progress);
//                AlertDialog Demo
                AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
                dialog.setTitle(String.format("Title 进度---%s", progress));
                dialog.setMessage(String.format("Message 进度---%s", progress));
                dialog.setCancelable(false);
                dialog.setPositiveButton("ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.d("MainActivity","It's ok");
                    }
                });
                dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.d("MainActivity", "取消");
                    }
                });
                dialog.show();
                break;
            default:
                break;
        }

    }
}

3.2.7 ProgressDialog

progressDialog和AlertDialog有点类似,都是弹出一个对话框,不同的是,ProgressDialog会在对话框中先是一个进度条,一般用于表示当前操作耗时.另外当数据加载后,必须带调用ProgressDialog的dismiss()方法来关闭对话框.
现在模拟一个ProgressDialog,在打开对话框后向另一个Activity发起一个Intent,并获取其返回的数据,数据返回后,关闭此progressDialog.
新建一个activity起名ProgressDialogBack,用来返回数据给MainAcitivity.代码如下:

public class ProgressDialogBack extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_progress_dialog_back);
        Intent intent1  = new Intent();
        intent1.putExtra("data_return","data is ok");
        setResult(RESULT_OK,intent1);
        finish();
    }
}

修改activity_main.xml及MainActivity.java,增加一个button并绑定事件,生成一个progressDialog,同时获取返回值并关闭progressDialog.MainActivity.java代码如下activity_main.xml代码略.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText editText;
    private ImageView imageView;
    private ProgressBar progressBar;
    private ProgressDialog progressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        editText = findViewById(R.id.edit_text);
        imageView = findViewById(R.id.image_view);
        button.setOnClickListener(this);

        progressBar = findViewById(R.id.progress_bar);
        Button processButton = findViewById(R.id.progress_button);
        processButton.setOnClickListener(this);

        progressDialog = new ProgressDialog(MainActivity.this);
        Button progress_dialog_button = findViewById(R.id.progress_dialog_button);
        progress_dialog_button.setOnClickListener(this);


    }


    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
                imageView.setImageResource(R.drawable.img8);
                break;
            case R.id.progress_button:
//                修改可见性
//                if (progressBar.getVisibility() == View.GONE) {
//                    progressBar.setVisibility(View.VISIBLE);
//                } else {
//                    progressBar.setVisibility(View.GONE);
//                }
//                测试进度
                int progress = progressBar.getProgress();
                progress = progress + 10;
                progressBar.setProgress(progress);
//                AlertDialog Demo
                AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
                dialog.setTitle(String.format("Title 进度---%s", progress));
                dialog.setMessage(String.format("Message 进度---%s", progress));
                dialog.setCancelable(false);
                dialog.setPositiveButton("ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.d("MainActivity", "It's ok");
                    }
                });
                dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.d("MainActivity", "取消");
                    }
                });
                dialog.show();
                break;
            case R.id.progress_dialog_button:
                progressDialog.setTitle("This is Progress Dialog");
                progressDialog.setMessage("Loading...");
                progressDialog.setCancelable(true);
                progressDialog.show();
                Intent intent = new Intent(MainActivity.this, ProgressDialogBack.class);
                startActivityForResult(intent, 1);
                break;
            default:
                break;
        }
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case 1:
                if (resultCode == RESULT_OK) {
                    String returnData = data.getStringExtra("data_return");
                    Log.d("MainActivity", returnData);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                progressDialog.dismiss();
                break;
            default:
        }
    }
}

需要注意的是,progressDialog已经被弃用,官方指南中有说明如下:

注意:Android 包含另一种名为 ProgressDialog 的对话框类,该类可显示带有进度条的对话框.不推荐使用此微件,因为它会在显示进度的情况下阻止用户与应用交互.如果需要指示加载进度或不确定的进度,您应遵循进度和 Activity 的设计指南,并在布局中使用 ProgressBar,而非 ProgressDialog.

该类已在API级别26中弃用.
ProgressDialog是模式对话框,可防止用户与应用进行交互.而不是使用此类,您应该使用进度指示器(例如)android.widget.ProgressBar,可以将其嵌入到应用的用户界面中.或者,您可以使用通知来通知用户任务的进度.

3.3 4种基本布局

3.3.1 线性布局

LinearLayout 线性布局

  • android:orientation="vertical" 垂直排列
  • android:orientation="horizontal" 水平排列
  • android:layout_height="wrap_content" 强制性地使视图扩展以显示全部内容
  • android:layout_width="match_parent" 布满整个屏幕

使用horizontal水平排列时,不能将控件的宽度指为match_parent,否则一个控件将在水平方向占满

  • android:layout_gravity="center" 布局控件的对齐方式

修改activity_progress_dialog_back.xml来测试线性布局
在有属性android:layout_weight时,android:layout_width属性就不会生效,指定成0dp时规范的写法.dp是Android中用于指定控件大小\间距等单位.
android:layout_weight="1" 表示EditText和Button在水平方向平分,系统会把LinearLayout下所有的控件的layout_weight值相加,并计算每个控件占据屏幕宽度的比例.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Typing"
        />
    <Button
        android:id="@+id/send"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="send"/>
</LinearLayout>

为了获取更好的展示效果,修改布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Typing"
        />
    <Button
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="send"/>
</LinearLayout>

3.3.2 相对布局

3.3.3 帧布局

3.3.4 百分比布局

3.4 自定义控件

View是Android中最基本的UI组件,ViewGroup是一种特殊的View,用于防止控件和布局容器.新建一个UICustomViews项目进行测试自定义组件.

3.4.1 引入布局

在layout下新建一个布局文件,起名为title.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/title_bg">

    <Button
        android:id="@+id/title_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:background="@drawable/back_bg"
        android:text="BACK"
        android:textColor="#fff"/>

    <TextView
        android:id="@+id/title_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_weight="1"
        android:textColor="#fff"
        android:text="Title Text"
        android:textSize="24sp"/>

    <Button
        android:id="@+id/title_edit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@drawable/edit_bg"
        android:layout_margin="5dp"
        android:text="Edit"
        android:textColor="#fff"/>
</LinearLayout>

在此布局中有两个按钮,一个TextView.左边的Button为返回键,右侧为编辑,中间显示标题.背景使用颜色或图片.代码中android:layout_margin="5dp"属性表示空间在上下左右方向的便宜距离.修改activity_main.xml中代码,将title.xml的布局引入,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include layout="@layout/title"/>
</LinearLayout>

最后修改MainActivity.java将系统中自带的标题栏隐掉,代码如下:


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }
    }
}

3.4.2 自定义控件

引入布局解决了重复编写布局代码问题,但是如果布局中的一些控件需要进行响应事件,则需要使用自定义控件方式解决.新建TitleLayout类并继承LinearLayout,作为自定义的标题控件.代码如下:


public class TitleLayout extends LinearLayout {
    public TitleLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title, this);
    }
}

重写LinearLayout中的带有两个参数的构造函数,from()方法可以构建出一个LayoutInflater对象,inflate()方法用来动态加载布局文件,inflate方法接收两个参数,一个是布局文件,一个是加载布局文件的父布局,这里为当前对象.

自定义控件创建好后,修改activity_main.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
    android:layout_height="match_parent">
<!--    <include layout="@layout/title"/>-->
    <com.example.uicustomviews.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

添加自定义控件的时候,需要指明控件的完整类名,修改TitleLayout中的代码,给按钮注册事件,代码如下:

public class TitleLayout extends LinearLayout {
    public TitleLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title, this);
        Button titleBackButton = findViewById(R.id.title_back);
        Button titleEditButton = findViewById(R.id.title_edit);
        titleBackButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                ((Activity) getContext()).finish();
            }
        });
        titleEditButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getContext(),"You click edit button",Toast.LENGTH_SHORT).show();
            }
        });
    }
}

此时自定义控件封装完毕.

3.5 ListView

3.5.1 ListView的简单用法

新建一个ListViewTest项目,修改activity_main.xml中代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

修改MainActivity.java中代码,初始化一些测试数据,

public class MainActivity extends AppCompatActivity {

    private String[] data = new String[20];

    private void initData() {
        StringBuilder stringBuilder = new StringBuilder("Cherry");
        for (int i = 0; i < 20; i++) {
            data[i] = stringBuilder.append(i).toString();
            stringBuilder.setLength(6);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                MainActivity.this, android.R.layout.simple_list_item_1, data);
        ListView listView = findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
}

在这里数组是无法单独传递给ListView的,需要使用Android提供的适配器实现类,这里使用的是ArrayAdapter,此外还有CursorAdapter、SimpleAdapter等.ArrayAdapter有多种构造函数,次数使用的是传入构造函数的上下文,子布局id及对应的数据.其中上下文这里为当前类,子布局id为android.R.layout.simple_list_item_1,这是Android内置的布局文件,内容只有一个TextView.最后调用setAdapter完成关联.

3.5.2 定制ListView的界面

上面的例子中,内置的子布局只有一个TextView,现在做一个自定义的ListView,为数据显示一组图片(图片文件提前复制到drawable内).首先建立一个Fruit类,用来表示数据的名称和id(此id为Android组件生成的R.id),代码如下:

public class Fruit {
    private String name;
    private int imageId;

    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getImageId() {
        return imageId;
    }

    public void setImageId(int imageId) {
        this.imageId = imageId;
    }
}

添加一个ListView的子项的自定义布局文件,起名为fruit_item.xml.再上一个例子中使用的是Android内置的布局文件android.R.layout.simple_list_item_1.fruit_item.xml内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="40dp"
        android:layout_height="40dp"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="10dp" />
</LinearLayout>

现在创建一个自定义的适配器类,并继承ArrayAdapter,通过泛型指定为Fruit类.新建的类名为FruitAdapter,代码如下:

public class FruitAdapter extends ArrayAdapter {

    private int resourceId;

    public FruitAdapter(@NonNull Context context, int resource, List<Fruit> objects) {
        super(context, resource,objects);
        resourceId = resource;
    }

    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
//        return super.getView(position, convertView, parent);
        Fruit fruit = (Fruit) getItem(position);
        View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        ImageView fruitImage = view.findViewById(R.id.fruit_image);
        TextView fruitName = view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

FruitAdapter重写了父类的构造函数,用于将上下文、ListView的子项布局的id和数据都传递进来.另外重写了getView()方法,此方法会在每个子项目滚动到屏幕内的时候调用,在此方法中,想通过getItem()方法获取当前位置的Fruit实例,然后使用LayoutInflater来加载子布局.LayoutInflater的使用在上一个例子(3.4.1)中已经使用过,而这里的第三个参数false表示只让我们在父布局中声明的layout属性生效,但不为这个View添加父布局(这块暂时没太理解,mark).后面代码比较简单,即设置当前位置的对象的值,将传入的值和自定义 子布局中的控件关联上.

修改MainActivity中的代码如下:

public class MainActivity extends AppCompatActivity {

    private String[] data = new String[20];
    private List<Fruit> fruitList = new ArrayList<Fruit>(20);

    private void initData() {
        StringBuilder stringBuilder = new StringBuilder("Cherry");
        for (int i = 0; i < 20; i++) {
            data[i] = stringBuilder.append(i).toString();
            stringBuilder.setLength(6);
        }
    }

    /**
     * 自定义子布局初始化数据
     */
    private void initFruit(){
        Fruit fruit =null;
        StringBuilder stringBuilder = new StringBuilder("Cherry");
        StringBuilder drawableName = new StringBuilder("shuzi");
        for (int i = 1; i < 21; i++) {
            int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
            fruit = new Fruit(stringBuilder.append(i).toString(),resID);
            fruitList.add(fruit);
            stringBuilder.setLength(6);
            drawableName.setLength(5);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        initData();
//
//        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
//                MainActivity.this, android.R.layout.simple_list_item_1, data);
//        ListView listView = findViewById(R.id.list_view);
//        listView.setAdapter(adapter);

        initFruit();
        FruitAdapter fruitAdapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
        ListView listview = findViewById(R.id.list_view);
        listview.setAdapter(fruitAdapter);
    }
}

这里为了方便动态的加载图片文件名称,使用了以下方式来获取图片的id

 int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());

如果单独初始化一个对象,应该按以下方式使用:

Fruit fruit = new Fruit("水果",R.drawable.xxx);

3.5.3 提升ListView的运行效率

在3.5.2的例子中,FruitAdapter的getView()方法每次都将布局重新加载了一遍,当ListView快速滚动式,可能造成系统瓶颈.在getView方法中有一个convertView的参数,这个参数可以将布局进行缓存,修改FruitAdapter

public class FruitAdapter extends ArrayAdapter {
   ...
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit = (Fruit) getItem(position);
        View view;
//        View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        }else {
            view = convertView;
        }
        ImageView fruitImage = view.findViewById(R.id.fruit_image);
        TextView fruitName = view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

此时,可以提高ListView的运行效率.但在获取fruitImage和fruitName时仍然回去访问控件实例,此时可以借助新建一个ViewHolder的类进行性能优化,在FruitAdapter中新建内部类ViewHolder,完整代码如下:

public class FruitAdapter extends ArrayAdapter {

    private int resourceId;

    public FruitAdapter(@NonNull Context context, int resource, List<Fruit> objects) {
        super(context, resource, objects);
        resourceId = resource;
    }

    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit = (Fruit) getItem(position);
        View view;
        ViewHolder viewHolder;
//        View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
            viewHolder = new ViewHolder();
            viewHolder.imageView = view.findViewById(R.id.fruit_image);
            viewHolder.textView = view.findViewById(R.id.fruit_name);
            view.setTag(viewHolder);//将ViewHolder存储到View中
        }else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.imageView.setImageResource(fruit.getImageId());
        viewHolder.textView.setText(fruit.getName());
//        ImageView fruitImage = view.findViewById(R.id.fruit_image);
//        TextView fruitName = view.findViewById(R.id.fruit_name);
//        fruitImage.setImageResource(fruit.getImageId());
//        fruitName.setText(fruit.getName());
        return view;
    }
    class ViewHolder{
        ImageView imageView;
        TextView textView;
    }
}

setTag()方法的介绍如下,按照个人理解,使用View的Tag可以存储一些数据对象而不需要自定义其他的数据结构,并在需要时通过getTag取出来,这样就完成了数据的缓存

/**
  * Sets the tag associated with this view. A tag can be used to mark
  * a view in its hierarchy and does not have to be unique within the
  * hierarchy. Tags can also be used to store data within a view without
  * resorting to another data structure.
  *
  * @param tag an Object to tag the view with
  *
  * @see #getTag()
  * @see #setTag(int, Object)
  */
 public void setTag(final Object tag) {
     mTag = tag;
 }

3.5.4 ListView的点击事件

如果ListView中的子项不能点击的话就没什么意义了,修改MainActivity中代码响应点击事件

public class MainActivity extends AppCompatActivity {
   ...
    private List<Fruit> fruitList = new ArrayList<Fruit>(20);
   ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        initData();
//
//        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
//                MainActivity.this, android.R.layout.simple_list_item_1, data);
//        ListView listView = findViewById(R.id.list_view);
//        listView.setAdapter(adapter);

        initFruit();
        FruitAdapter fruitAdapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
        ListView listview = findViewById(R.id.list_view);
        listview.setAdapter(fruitAdapter);
//        子布局的点击事件
        listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                String imgMessage = String.format("Click img %s", position+1);
                Fruit fruit = fruitList.get(position);
                Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
                Log.d("MainActivity",fruit.getName());
            }
        });
    }
}

此时当点击ListView中的任何一个子项目时,就会调用onItemClick方法,根据position来判断用户点击的是哪个子项目.这里的position和id分别表示view的位置和row的id,如下介绍

 /**
  * Callback method to be invoked when an item in this AdapterView has
  * been clicked.
  * <p>
  * Implementers can call getItemAtPosition(position) if they need
  * to access the data associated with the selected item.
  *
  * @param parent The AdapterView where the click happened.
  * @param view The view within the AdapterView that was clicked (this
  *            will be a view provided by the adapter)
  * @param position The position of the view in the adapter.
  * @param id The row id of the item that was clicked.
  */
 void onItemClick(AdapterView<?> parent, View view, int position, long id);

根据以上例子,考虑如果想在子布局的每一项单独在绑定一个事件,那么在FruitAdapter中实现、在MainActivity中实现、两个类中都实现,那么响应用户点击的顺序是什么样呢?于是修改代码FruitAdapter及MainActivity如下所示:
FruitAdapter.java

public class FruitAdapter extends ArrayAdapter {

    private int resourceId;

    public FruitAdapter(@NonNull Context context, int resource, List<Fruit> objects) {
        super(context, resource, objects);
        resourceId = resource;
    }

    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        Fruit fruit = (Fruit) getItem(position);
        View view;
        ViewHolder viewHolder;
//        View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
            viewHolder = new ViewHolder();
            viewHolder.imageView = view.findViewById(R.id.fruit_image);
            viewHolder.textView = view.findViewById(R.id.fruit_name);
            view.setTag(viewHolder);//将ViewHolder存储到View中
        }else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.imageView.setImageResource(fruit.getImageId());
        viewHolder.textView.setText(fruit.getName());
        //----
        //测试绑定点击响应
        viewHolder.imageView.setOnClickListener(v -> Log.d("FruitAdapter","FruitAdapter click event"));
        //-----
//        ImageView fruitImage = view.findViewById(R.id.fruit_image);
//        TextView fruitName = view.findViewById(R.id.fruit_name);
//        fruitImage.setImageResource(fruit.getImageId());
//        fruitName.setText(fruit.getName());
        return view;
    }
    class ViewHolder{
        ImageView imageView;
        TextView textView;
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private String[] data = new String[20];
    private List<Fruit> fruitList = new ArrayList<Fruit>(20);

    private void initData() {
        StringBuilder stringBuilder = new StringBuilder("Cherry");
        for (int i = 0; i < 20; i++) {
            data[i] = stringBuilder.append(i).toString();
            stringBuilder.setLength(6);
        }
    }

    /**
     * 自定义子布局初始化数据
     */
    private void initFruit(){
        Fruit fruit =null;
        StringBuilder stringBuilder = new StringBuilder("Cherry");
        StringBuilder drawableName = new StringBuilder("shuzi");
        for (int i = 1; i < 21; i++) {
            int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
            fruit = new Fruit(stringBuilder.append(i).toString(),resID);
            fruitList.add(fruit);
            stringBuilder.setLength(6);
            drawableName.setLength(5);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        initData();
//
//        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
//                MainActivity.this, android.R.layout.simple_list_item_1, data);
//        ListView listView = findViewById(R.id.list_view);
//        listView.setAdapter(adapter);

        initFruit();
        FruitAdapter fruitAdapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
        ListView listview = findViewById(R.id.list_view);
        listview.setAdapter(fruitAdapter);
//        子布局的点击事件
        listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                String imgMessage = String.format("Click img %s", position+1);
                Fruit fruit = fruitList.get(position);

                Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
                Log.d("MainActivity",fruit.getName());
                //---
                //测试获取子组件绑定点击事件
//              ImageView imageView =  view.findViewById(R.id.fruit_image);
                ImageView imageView = (ImageView) ((LinearLayout)view).getChildAt(0);
                imageView.setOnClickListener(v -> {
                    Log.d("MainActivity",imgMessage);
                    Toast.makeText(MainActivity.this,imgMessage,Toast.LENGTH_SHORT).show();
                });
                //-----
            }
        });
    }
}

测试发现如果在FruitAdapter中和MainActivity中同时加入imageView的点击,并不会多次执行,但是每次点击时,响应的事件却不同.此处作为遗留问题,后续继续研究下.

3.6 RecyclerView

ListView需要优化运行效率,同时只能实现纵向滚动,无法横向滚动.而RecyclerView可以说是一个增强的ListView.新建一个RecycleViewTest项目来测试.

3.6.1 RecyclerView的基本用法

原书中描述RecyclerView定义在了support库中,但是本次测试时基于android9(API-28),所以略有差异.根据官方指南,在gradle中增加依赖,如下.增加后需要同步一下.

dependencies {
    implementation "androidx.recyclerview:recyclerview:1.1.0"
    // For control over item selection of both touch and mouse driven selection
    implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01"
   }

修改activity_main.xml中代码,修改为LinearLayout布局并增加RecyclerView组件,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

在上面代码中,因为RecyclerView不是SDK内置组件,所以需要指定完整的包路径.

之后将3.5中的Fruit与fruit_item.xml的布局文件及图片文件复制进来即可.新建一个FruitAdapter的适配器类,并且继承RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder.其中ViewHolder为FruitAdapter的内部类,完整代码如下:

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {

    private List<Fruit> mFruitList;

    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView fruitImage;
        TextView fruitText;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            fruitImage = itemView.findViewById(R.id.fruit_image);
            fruitText = itemView.findViewById(R.id.fruit_name);
        }
    }

    public FruitAdapter(List<Fruit> fruitList) {
        this.mFruitList = fruitList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitText.setText(fruit.getName());
    }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }


}

首先,内部类ViewHolder,这个ViewHolder需要继承RecyclerView.ViewHolder.同时构造方法需要传入一个View参数,这个参数对应的通常是RecyclerView子项的最外层布局,这样就可以通过findViewById()方法来获取布局中的ImageView和TextView组件实例.

FruitAdapter也有一个构造函数,用来传入数据源,这里是mFruitList.

FruitAdapter继承了RecyclerView.Adapter,那么就必须重写onCreateViewHolder()、onBindViewHolder()、getItemCount()这三个方法.

  • onCreateViewHolder() 用来创建ViewHolder实例,并把加载的布局传入到构造函数中,最后返回对应的泛型实例,这里是ViewHolder
  • onBindViewHolder() 用来对子项数据进行赋值,会在每个子项数据滚动到屏幕内时执行,通过position参数获取当前的实例对象
  • getItemCount() 用来告诉RecyclerView数据一共有多少

修改MainActivity中代码,并测试结果

public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>(20);

    /**
     * 自定义子布局初始化数据
     */
    private void initFruits(){
        Fruit fruit =null;
        String nameStr = "Cherry%s";
        StringBuilder drawableName = new StringBuilder("shuzi");
        for (int i = 1; i < 21; i++) {
            int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
            fruit = new Fruit(String.format(nameStr,i),resID);
            fruitList.add(fruit);
            drawableName.setLength(5);
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initFruits();
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(linearLayoutManager);
        FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(fruitAdapter);
    }
}

LayoutManager用于指定RecyclerView的布局方式,这里使用的是LIneraLayoutManager.S

3.6.2 实现横向滚动和瀑布流布局

首先对fruit_item.xml布局进行修改,因为目前的这个里的布局是水平排列,适用于纵向滚动的场景.要实现横向滚动,则需要把fruit_item中的布局改为垂直排列比较合理,修改fruit_item.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<!--<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<!--    android:layout_width="match_parent"-->
<!--    android:layout_height="wrap_content">-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="#85C1E9"
        android:layout_gravity="center"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"/>
</LinearLayout>

将LinearLayout改为垂直排列后,把宽度调整为100dp,之后将ImageView与TextView的布局改为水平居中.修改MainActivity中代码,将布局管理器中的布局改为Horizon.这里是将LinearLayoutManager中设置为Horizonal.代码如下:


public class MainActivity extends AppCompatActivity {
   ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(linearLayoutManager);
        FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(fruitAdapter);
    }
  ...
}

在RecyclerView中,将布局的排列交给了LayoutManager管理,子类只需要按照LayoutManager的规范了来定义就可以实现不通的排列布局.
除了LinearLayout外,RecyclerView还提供了GridLayoutManager和StaggeredGridManager这两种内置的布局排列.GirdLayoutManager用于实现网格的布局,StaggeredGridLayoutManager用来实现瀑布流式布局.下面来实现一个瀑布流布局.
复制一个fruit_item.xml起名为fruit_item_staggered.xml,修改其中代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">

    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="#85C1E9"
        android:layout_gravity="center_horizontal"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp"/>
</LinearLayout>

这里将宽度修改为了match_parent,因为瀑布流布局的宽度应该有布局的列数来自动计算.
修改FruitAdapter.java中布局的引用

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_staggered, parent, false);
//        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

修改MainActivity.java中的布局管理器部分,


public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>(20);

    private  String getRandomLengthName(String name){
        Random random = new Random();
        int length = random.nextInt(20)+1;
        StringBuilder builder = new StringBuilder();
        for(int i=0;i<length;i++){
            builder.append(name);
        }
        return builder.toString();
    }
    /**
     * 自定义子布局初始化数据
     */
    private void initFruits(){
        Fruit fruit =null;
        String nameStr = "Cherry%s";
        StringBuilder drawableName = new StringBuilder("shuzi");
        for (int i = 1; i < 21; i++) {
            int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
            fruit = new Fruit(getRandomLengthName(String.format(nameStr,i)),resID);
            fruitList.add(fruit);
            drawableName.setLength(5);
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initFruits();
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
//        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
//        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
//        recyclerView.setLayoutManager(linearLayoutManager);
        StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(staggeredGridLayoutManager);
        FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(fruitAdapter);
    }
}

这里创建了一个StaggeredGridLayoutManager 的布局管理器,第一个参数3表示布局将分3列,第二个参数指定布局的排列方向.getRandomLengthName()方法用来创建一个随机数并修改Fruit的名字,将名字变得长短不一,来测试子项的高度.

在测试一下GridLayoutManager,同样复制一下fruit_item.xml起名字为fruit_item_grid.xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">

    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="#85C1E9"
        android:layout_gravity="center_horizontal"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp"/>
</LinearLayout>

修改FruitAdapter.java中布局的引用

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_grid, parent, false);
//        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_staggered, parent, false);
//        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

修改MainActivity.java中的布局管理器部分,

public class MainActivity extends AppCompatActivity {
   ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initFruits();
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
//        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
//        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
//        recyclerView.setLayoutManager(linearLayoutManager);
//        StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
//        recyclerView.setLayoutManager(staggeredGridLayoutManager);
        GridLayoutManager gridLayoutManager = new GridLayoutManager(MainActivity.this,4);
        recyclerView.setLayoutManager(gridLayoutManager);
        FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(fruitAdapter);
    }
}

这里的GridLayoutManager构造方法,根据API的描述,第一个参数为当前的Context,第二个参数为Grid的列数

/**
     * Creates a vertical GridLayoutManager
     *
     * @param context Current context, will be used to access resources.
     * @param spanCount The number of columns in the grid
     */
    public GridLayoutManager(Context context, int spanCount) {
        super(context);
        setSpanCount(spanCount);
    }

3.6.3 RecyclerView的点击事件

在FruitAdapter.java中修改RecyclerView的注册点击事件,修改代码如下:

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    private List<Fruit> mFruitList;

    static class ViewHolder extends RecyclerView.ViewHolder {
        View fruitView;
        ImageView fruitImage;
        TextView fruitText;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            this.fruitView = itemView;
            fruitImage = itemView.findViewById(R.id.fruit_image);
            fruitText = itemView.findViewById(R.id.fruit_name);
        }
    }

    public FruitAdapter(List<Fruit> fruitList) {
        this.mFruitList = fruitList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, final int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_grid, parent, false);
//        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_staggered, parent, false);
//        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
//        ViewHolder viewHolder = new ViewHolder(view);
        final ViewHolder viewHolder = new ViewHolder(view);
        viewHolder.fruitView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = viewHolder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Toast.makeText(view.getContext(), "you click view" + fruit.getName(), Toast.LENGTH_SHORT).show();
            }
        });
        viewHolder.fruitImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = viewHolder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Toast.makeText(view.getContext(),"You click Image"+fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitText.setText(fruit.getName());
    }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }
}

在内部类ViewHolder中增加了一个fruitVIew的变量来保存子组件的最外层实例,这样在监听点击动作时可以监听到最外层的组件.这里分别注册的最外层组件的点击事件及图片部分的点击事件.当用户点击不通区域时,则调用不同的事件.用户点击文字部分时,因为没有注册任何事件,则此事件被最外层布局捕捉到.

3.7 编写界面的最佳实践

这里需要先创建一个UIBestPractice的项目,修改activity_main.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/message_left">
</LinearLayout>

其中message_left如果使用普通的图片,则由于宽度问题图片可能被拉伸,此时的效果非常差,所以需要使用Android SDK自带的draw9patch来编辑图片

3.7.1 制作Nine-Patch图片

书中描述在Android Studio内置的jdk中有对应的工具,为Android Studio安装目录>/jre/bin,但测试时没有找到.将图片复制到idea中,选中图片,右键.此时有create 9-path file的选项,通过此功能也可以制作Nine-Patch图片.
自己测试的几个图片,不太熟练.直接下载书中源码里图片继续测试.

3.7.2 聊天界面

聊天界面的例子主要分两个部分,一个是RecyclerVeiw实现的子组件来显示消息,一个是通过Text和Button组成的发送消息.修改activity_main.xml的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d8e0e8">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/msg_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:id="@+id/input_text"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:hint="Type something  here"
            android:layout_height="wrap_content"
            android:maxLines="2"/>
        <Button
            android:id="@+id/send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send"/>
    </LinearLayout>
</LinearLayout>

创建一个实体类Msg来定义消息,主要属性有消息内容及消息类型(接收的消息和发送的消息),Msg.java如下


public class Msg {
    public static final int TYPE_RECEIVED = 0;
    public static final int TYPE_SEND = 1;
    private String content;
    private int type;

    public Msg(String content, int type) {
        this.content = content;
        this.type = type;
    }
	... setter and getter
}

根据RecyclerView的使用方式,需要一个Adapter和layoutManager.先创建一个MsgAdapter类,在类中实现ViewHolder的静态类及存放消息列表的属性,代码如下:

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {

    private List<Msg> mMsgList;

    static class ViewHolder extends RecyclerView.ViewHolder{
        LinearLayout leftlayout;
        LinearLayout rightLayout;
        TextView leftMsg;
        TextView rightMsg;

        public ViewHolder(@NonNull View view) {
            super(view);
            leftlayout = view.findViewById(R.id.left_layout);
            rightLayout = view.findViewById(R.id.right_layout);
            leftMsg = view.findViewById(R.id.left_msg);
            rightMsg = view.findViewById(R.id.right_msg);
        }
    }

    public MsgAdapter(List<Msg> mMsgList) {
        this.mMsgList = mMsgList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Msg msg = mMsgList.get(position);
        if (msg.getType() == Msg.TYPE_RECEIVED) {
            holder.leftlayout.setVisibility(View.VISIBLE);
            holder.rightLayout.setVisibility(View.GONE);
            holder.leftMsg.setText(msg.getContent());
        } else {
            holder.leftlayout.setVisibility(View.GONE);
            holder.rightLayout.setVisibility(View.VISIBLE);
            holder.rightMsg.setText(msg.getContent());
        }

    }

    @Override
    public int getItemCount() {
        return mMsgList.size();
    }

}

这里和上节中的例子基本一致.只是在onBindViewHolder()中增加了对消息类型的判断,如果是收到消息,则显示左侧消息布局,隐藏右侧.如果是发出的消息,就隐藏左侧显示右侧.现在修改MainActivity.java中的代码,

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private EditText inputText;
    private Button sendButton;
    private MsgAdapter msgAdapter;
    private List<Msg> msgList = new ArrayList<>();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        recyclerView = findViewById(R.id.msg_recycler_view);
        inputText = findViewById(R.id.input_text);
        sendButton = findViewById(R.id.send);

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        msgAdapter = new MsgAdapter(msgList);
        recyclerView.setAdapter(msgAdapter);

        sendButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String content = inputText.getText().toString();
                if (!"".equals(content)) {
                    Msg msg = new Msg(content, getRandomType());
                    msgList.add(msg);
//                    当有新消息时刷新ListView中的显示
                    msgAdapter.notifyItemInserted(msgList.size() - 1);
//                    将ListView定位到最后一行
                    recyclerView.scrollToPosition(msgList.size() - 1);
                    inputText.setText("");
                }
            }
        });
    }
    //随机获取消息类型
    private int getRandomType() {
        Random random = new Random();
        if (random.nextInt(10) > 5) {
            return Msg.TYPE_SEND;
        } else {
            return Msg.TYPE_RECEIVED;
        }
    }
}

这里增加了一个getRandomType的方法,用来随机生成消息是发送的还是接收的.

遗留的问题

  • 笔记3.5.4 ListView的点击事件中,子布局中子组件点击事件加载顺序及是否会覆盖的问题
posted on 2020-07-15 19:24  学业未成  阅读(236)  评论(0编辑  收藏  举报