账号: test1 密码: test1
在 Android 中,Activity 的生命周期由一系列回调方法组成, 用于管理 Activity 的创建、启动、暂停、恢复、停止和销毁等不同状态。 以下是常见的 Activity 生命周期方法(调用时机):
回调流程参考官方文档:
<com.flyjingfish.shapeimageviewlib.ShapeImageView/>属性
首先导包:
io.github.FlyJingFish:ShapeImageView:1.4.9
尺寸 图像的四个角是圆角的`
shape_left_top_radius
尺寸 图像的左上角是圆角的
shape_right_top_radius
尺寸 图像右上角的圆角
shape_right_bottom_radius
尺寸 图像的右下角是圆角的
shape_left_bottom_radius
尺寸 图像的左下角是圆角的
shape_start_top_radius
尺寸 图像左上角(Rtl:右上角)角圆角
shape_end_top_radius
尺寸 图像右上角(Rtl:左上角)角圆角
shape_end_bottom_radius
尺寸 图片右下角(Rtl:左下角)圆角
shape_start_bottom_radius
尺寸 图片左下角(Rtl:右下角)角圆角
shape_border
枚举 背景边框绘图形状为无绘图/矩形矩形/椭圆形圆形
shape_border_radius
尺寸 背景边框的四个角是圆形的
shape_border_left_top_radius
尺寸 背景边框的左上角是圆角的
shape_border_right_top_radius
尺寸 背景边框右上角的圆角
shape_border_right_bottom_radius
尺寸 背景边框的右下角是圆形的
shape_border_left_bottom_radius
尺寸 背景边框的左下角是圆形的
shape_border_start_top_radius
尺寸 背景边框左上角(Rtl:右上角)角圆角
shape_border_end_top_radius
尺寸 背景边框右上角(Rtl:左上角)角圆角
shape_border_end_bottom_radius
尺寸 背景边框右下角(Rtl:左下角)角圆角
shape_border_start_bottom_radius
尺寸 背景边框的左下角(Rtl:右下角)是圆形的
shape_border_color
颜色 背景边框绘图颜色
shape_border_gradient
布尔 背景边框是否以渐变色绘制
shape_border_startColor
颜色 背景边框绘制渐变颜色开始颜色
shape_border_centerColor
颜色 背景边框绘制渐变中间颜色
shape_border_endColor
颜色 背景边框绘制渐变色结束颜色
shape_border_angle
浮 背景边框绘制渐变颜色的起始角度
shape_border_rtl_angle
布尔 背景边框的渐变起始角度是否支持镜像 Rtl 适配
shape_border_strokeWidth
尺寸 背景边框绘图画笔的宽度
autoCrop_height_width_ratio
浮 图像纵横比是视图纵横比的倍数
直接将图片放入drawable-xxhdpi等文件夹下,系统自动根据手机的分辨率选择 合适的图片。
图标一般选择svg图片,右击drawable文件夹,选择新建,再选择Vector Asset 在导入窗口中clip art是系统自带的icon,local-file则是导入本地svg或者psd 图标。关于图标,也可以放在mipmap文件夹中。
记录了各种值的引用的文件夹,方便修改。
1、attr.xml:
自定义类的属性,例如自定义的圆形图片类,该类的部分属性可以在此设置。
2、colors.xml
存放各种颜色的文件,若同一种颜色频繁使用,可以在这里定义此颜色,并使用 @colors/name来引用此颜色
3、dimens.xml
定义了各种长度及大小的数值,引用方法:@dimens/name
4、refs.xml
定义了对某些资源的引用, 引用方法:@refs/name
5、string.xml
定义TextView的文本,系统不推荐硬编码文本,推荐将文本统一存放在这里, 引用方法:android:text="@string/name"
6、themes.xml
定义某些通用的控件的样式,包括系统主题的样式 引用方法:style="@style/name"
核心方法:startActivity(),参数为一个意图Intent,以下为一个实例:
TextView buttonBack1 = findViewById(R.id.main_ButtonBack);
//寻找触发事件的控件main_ButtonBack
buttonBack1.setOnClickListener(view -> { //setOnClickListener为控件设置监听,view为匿名类,此处已经简化
startActivity(new Intent(RecyclerViewActivity.this, MainActivity.class));
//Intent的第一个参数是所在的activity.this,表示出发点
//第二个参数表示目的布局所在的activity,注意是后缀是.class!!!
});
值得注意的是:涉及到的activity,必须在AndroidManifest.xml文件中注册才可以使用
将上述代码存放在一个方法中,在onCreate()初始化页面时,完成对控件的初始化,增加代码可读性
Intent不止可以进行页面跳转,也可以传递数据。
设置数据:putExtra(),取出:get类型Extra(),类型是你想要取的数据的类型
putExtra()的参数是键值对(key-value),第一个参数为名称,第二个为值
get()的参数一般只有一个,即是想要取的数据的名称,有时会设置第二个参数为默认值
上级页面在跳转时使用 :
private static final int REQUEST_CODE = 0x10;
startActivityForResult(intent, REQUEST_CODE);
其中 intent时意图,REQUEST_CODE是int型的请求码 下级页面完成事件时调用如下代码:
// 结果码
Intent data = new Intent();
data.putExtra("KEY_RESULT", "退出成功!");
setResult(Activity.RESULT_OK, data);
// 结束当前Activity,但是返回的是上一个页面,请注意使用
finish();
最后在上级页面中重写onActivityResult方法
结果码必须是Activity.RESULT_OK,代表返回成功
/**接收页面返回的结果@param requestCode 请求码@param resultCode 结果码@param data 返回数据集
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE) {
String result = data != null ? data.getStringExtra("KEY_RESULT") : "空字符串";
Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show();
Log.e("返回结果", result);
}
}
}
Toast.makeText(MainActivity.this, "用户名或者密码错误", Toast.LENGTH_SHORT).show();
//第一个参数为调用此方法的上下文即是所在的activity,第二个参数是提示文本,第三个参数是提示显示的时长
//最后调用.show()将提示显示出来
Android 中的 recycleView 是一种视图容器,用于显示多个可重用的组件, 例如列表、网格和画廊。recycleView 可以自动管理组件的大小和布局,以便在内存和屏幕上最大化使用资源。
recycleView 的特点是可以重用组件,这意味着开发人员不需要手动创建和管理每个组件。 recycleView 还可以自动处理滚动和缩放,使开发人员可以专注于编写列表或画廊的 UI。
创建item_list.xml布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.demo1.CircleImageView
android:id="@+id/group_avatar_img"
android:layout_height="50dp"
android:layout_width="50dp"
android:src="@drawable/img_1"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:textSize="20sp"
android:textColor="#000000"
android:text="@string/group_name"
android:layout_toEndOf="@id/group_avatar_img"
android:layout_alignTop="@id/group_avatar_img"
/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="15sp"
android:textColor="#000000"
android:text="@string/last_msg"
android:layout_toEndOf="@id/group_avatar_img"
android:layout_below="@id/textView"
android:maxLength="30"
/>
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginTop="20dp"
android:textSize="15sp"
android:textColor="#000000"
android:text="@string/last_msg_date"
android:layout_alignParentEnd="true"
android:maxLength="30"
/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
效果如图:
public class News {
public String title; // 标题
public String content; //内容
public int img; //图片
public String date; //时间
}
图片资源是通过id获取,故设置为int,实际开发图片是通过后端返回,这里只做模拟数据
在activity_main.xml布局中添加如下代码
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="70dp"
android:layout_marginBottom="70dp"
android:paddingTop="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
此时布局效果如下:
此时布局内还未显示重用组件,默认显示item0到9
创建内部类MyAdapter(适配器)和MyViewHolder:
adapter是数据与ui之间的桥梁,它把后台数据与前端ui连接到一起,是一个展示数据的载体。
ViewHolder大部分都是通常出现在适配器里,为的就是listview滚动的时候能够快速设置值, 而不必每次都重新创建很多对象,从而提升性能。 在android开发中Listview是一个很重要的组件, 它以列表的形式根据数据的长自适应手机来展示具体内容,方便用户可以自由的定义listview每一列的布局,但当listview有大量的数据需要加载的时候, 会占据大量内存,影响性能,这时候就需要按需填充并重新使用view来减少对象的创建。
优先写泛型MyViewHolder
其次onCreateViewHolder
再写getItemCount
最后写onBindViewHolder
/**
* 适配器类
*/
class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
/**
* 初始化holder,将其与重用组件item_list.xml关联
* @param parent The ViewGroup into which the new View will be added after it is bound to
* an adapter position.
* @param viewType The view type of the new View.
*
* @return 数据源处理器
*/
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = View.inflate(MsgRecyclerViewActivity.this, R.layout.item_list, null);
return new MyViewHolder(view);
}
/**
* 使用holder动态设置绑定组件内容
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.mTitleTv.setText(news.title);
holder.mTitleContent.setText(news.content);
holder.circleImageView.setImageResource(news.img);
holder.date.setText(news.date);
}
/**
*
* @return 返回数据的长度
*/
@Override
public int getItemCount() {
return mNewsList != null ? mNewsList.size() : 0;
}
}
/**
* 自定义的ViewHolder
*/
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView mTitleTv; //这些属性的类型对应item_list.xml中的各个控件
TextView mTitleContent;
CircleImageView circleImageView;
TextView date;
/**
* 将数据源与布局对应
* @param itemView 数据源
*/
public MyViewHolder(@NonNull View itemView) { //构造函数
super(itemView);
mTitleTv = itemView.findViewById(R.id.textView); //对应重用布局的群名
mTitleContent = itemView.findViewById(R.id.textView2); //对应最新消息
circleImageView = itemView.findViewById(R.id.group_avatar_img); //对应群头像
date = itemView.findViewById(R.id.textView3); //对应时间
}
}
在java代码中初始化数据并使用适配器和holder:
定义全局变量
RecyclerView mRecyclerView; //接收RecycleView控件
MyAdapter mMyAdapter; //适配器
List<News> mNewsList = new ArrayList<>(); //数据列表
在onCreate()方法中添加:
mRecyclerView = findViewById(R.id.recyclerview); //绑定RecycleView控件
// 构造一些数据
@SuppressLint("SimpleDateFormat") SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");//格式化时间
for (int i = 0; i < 20; i++) { //初始化模拟数据,真实开发通过后端返回
News news = new News();
news.title = "群聊" + i;
news.content = "最新消息" + i;
news.img = R.drawable.img_2;
news.date =formatter.format( new Date());
mNewsList.add(news);
}
mMyAdapter = new MyAdapter();
mRecyclerView.setAdapter(mMyAdapter); //为RecycleView设置适配器
LinearLayoutManager layoutManager = new LinearLayoutManager(MsgRecyclerViewActivity.this);
//列表布局,第二参数不指定是默认垂直滚动
mRecyclerView.setLayoutManager(layoutManager); //为RecycleView设置布局适配器
当该布局生成时,RecycleView就会产生列表数据,效果如下:
生成一个最简单的弹出框
AlertDialog.Builder builder = new AlertDialog.Builder(ProfileActivity.this);
builder.setTitle("编辑资料");
builder.setView(R.layout.create_user_dialog); //弹出框的布局
builder.setPositiveButton("确定",null);
builder.setNegativeButton("返回",null);
AlertDialog alertDialog = builder.create();
alertDialog.show();
alertDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT,1000); //大小
效果图:
可能因为android版本的问题,部分人需要在build.gradle中添加
maven { url 'https://jitpack.io' }
然后在app目录下的build.gradle文件的依赖项里添加:
implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.11"
关键是继承BaseQuickAdapter<实体类,BaseViewHolder>
/**
* 第三方库使用RecyclerView的适配器
*/
public class TestAdapter extends BaseQuickAdapter<News, BaseViewHolder> {
//构造函数,传入重用布局的资源id
public TestAdapter(int layoutResId) {
super(layoutResId);
}
/**
*绑定数据
* @param holder 数据处理器
* @param news 数据实体类
*/
@Override
protected void convert(@NonNull BaseViewHolder holder, News news) {
//第一个参数是需要绑定数据的控件,第二个参数是需要被绑定的数据
holder.setText(R.id.textView, news.title).
setText(R.id.textView2, news.content)
.setText(R.id.textView3, news.date);
}
}
步骤与之前的基础用法相同
//定义全部变量
private TestAdapter adapter;
//然后在onCreate中初始化
mRecyclerView = findViewById(R.id.recyclerview); //绑定RecycleView控件
// 构造一些数据
@SuppressLint("SimpleDateFormat") SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");//格式化时间
for (int i = 0; i < 20; i++) { //初始化模拟数据,真实开发通过后端返回
News news = new News();
news.title = "群聊" + i;
news.content = "最新消息" + i;
news.img = R.drawable.img_2;
news.date =formatter.format( new Date());
mNewsList.add(news);
}
adapter = new TestAdapter(R.layout.item_list);
mRecyclerView.setAdapter(adapter); //为RecycleView设置适配器
// 添加数据
adapter.setList(mNewsList);
// adapter 点击事件
adapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
// 点击事件的逻辑代码
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(MsgRecyclerViewActivity.this);
//列表布局,第二参数不指定是默认垂直滚动
mRecyclerView.setLayoutManager(layoutManager); //为RecycleView设置布局适配器
值得注意的是,设置点击事件的方法有点小变化
方法从setOnClickListener变成了setOnItemClickListener
还有,重用的布局,是循环生成一整个布局,如过有高度宽度等需求,
必须自己手动设置好
相比之下确实已经简化了不少代码,同时也让适配器的概念清晰了不少。
右键包名-->新建-->Activity-->BottomNavigationActivity
语言选择java,其他的例如名字,自己取去吧!!
创建完成后,会在当前包下产生一个ui包,java文件BottomNavigationActivity
res文件夹下产生:
layout文件:
activity_main_bottom_navigation: 不同的fragment会在activity_main_bottom_navigation布局中替换它的fragment部分
fragment_home:第一个fragment的布局
fragment_dashboard:第二个fragment的布局
fragment_notifications:第三个fragment的布局
menu文件:bottom_nav_menu: 导航栏的样式
navigation文件:mobile_navigation: 导航栏与fragment布局的关联
需要添加固定控件的可以在该布局中添加
在res/menu/bottom_nav_menu文件中编写样式
记得自己替换成想要的样式
<item
android:id="@+id/navigation_home"
android:icon="@drawable/msg"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/friend"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/space"
android:title="@string/title_notifications" />
res/navigation/mobile_navigation中可以将fragment与导航栏进行关联
fragment意为一个片段,可以理解成为切换的一个视图,但其本质是Activity 的一部分,无论切换几次,都在该Activity的范围内
<fragment
android:id="@+id/navigation_home"
android:name="com.example.demo1.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<!--name属性对应Java类,对这个fragment进行初始化等操作
layout属性对应其页面布局
-->
例如:fragment_home文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/img_3"
tools:context=".ui.home.HomeFragment">
<include
android:id="@+id/main_avatar_bar"
layout="@layout/main_avatar_bar"
/>
<TextView
android:id="@+id/main_ButtonBack"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/back"
android:layout_marginTop="10dp"
tools:ignore="MissingConstraints,SpeakableTextPresentCheck"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="70dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
其中main_avatar_bar为:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="@color/main_user_avatar_background"
android:layout_height="70dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.demo1.CircleImageView
android:id="@+id/main_avatar_img"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:src="@drawable/avatar"
tools:ignore="MissingConstraints,SpeakableTextPresentCheck" />
<TextView
android:id="@+id/main_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:textSize="20sp"
android:text="@string/user_name"
android:layout_toEndOf="@id/main_avatar_img"
android:layout_alignTop="@id/main_avatar_img"
/>
<TextView
android:id="@+id/main_user_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="12sp"
android:text="@string/user_status"
android:layout_toEndOf="@id/main_avatar_img"
android:layout_below="@id/main_username"
android:maxLength="30"
/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
进入ui包中fragment对应的java类:HomeFragment
记得修改onCreateView中的代码,将复杂的代码稍微简化一下
添加点击事件等功能应该在onViewCreated中完成
这个fragment是一个列表数据,关于本视图内的RecyclerView的使用,请参考前文
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.demo1.BottomNavigationActivity;
import com.example.demo1.CircleImageView;
import com.example.demo1.MainActivity;
import com.example.demo1.MsgRecyclerViewActivity;
import com.example.demo1.ProfileActivity;
import com.example.demo1.R;
import com.example.demo1.adapter.TestAdapter;
import com.example.demo1.databinding.FragmentHomeBinding;
import com.example.demo1.entity.News;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class HomeFragment extends Fragment {
public RecyclerView recyclerView;
public List<News> newsList = new ArrayList<>(); //数据列表
public TestAdapter adapter;
/**
* 开始创建视图时进行的初始化
* @param inflater The LayoutInflater object that can be used to inflate
* any views in the fragment,
* @param container If non-null, this is the parent view that the fragment's
* UI should be attached to. The fragment should not add the view itself,
* but this can be used to generate the LayoutParams of the view.
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*
* @return
*/
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
return LayoutInflater.from(getContext()).inflate(R.layout.fragment_home, container, false);
}
/**
* 视图创建完成后要完成的操作
* @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recyclerview);
adapter = new TestAdapter(R.layout.item_list);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
//因为是在fragment中,无法直接使用类名.this获取上下文,故使用getContext()
loadData();
// adapter 点击事件
adapter.setOnItemClickListener((adapter, view1, position) -> {
// startActivity(new Intent(MsgRecyclerViewActivity.this, ));
Toast.makeText(getContext(), "点击了列表项!", Toast.LENGTH_SHORT).show();
});
backToLogin(view);
goToProfile(view);
}
/**
* 加载数据
*/
private void loadData(){
@SuppressLint("SimpleDateFormat") SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");//格式化时间
for (int i = 0; i < 20; i++) { //初始化模拟数据,真实开发通过后端返回
News news = new News();
news.title = "群聊" + i;
news.content = "最新消息" + i;
news.img = R.drawable.img_2;
news.date =formatter.format( new Date());
newsList.add(news);
}
adapter.setList(newsList);
}
/**
* 返回登录页按钮
* @param view 上下文
*/
public void backToLogin(View view) {
TextView buttonBack1 = view.findViewById(R.id.main_ButtonBack);
buttonBack1.setOnClickListener(view1 -> {
startActivity(new Intent(getContext(), MainActivity.class));
});
}
/**
* 跳转个人信息页按钮
* @param view 上下文
*/
public void goToProfile(View view){
CircleImageView buttonBack1 = view.findViewById(R.id.main_avatar_img);
buttonBack1.setOnClickListener(view1 -> {
startActivity(new Intent(getContext(), ProfileActivity.class));
});
}
}
去除部分不需要的代码后:
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.bottomnavigation.BottomNavigationView;
public class BottomNavigationActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_bottom_navigation);
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main_bottom_navigation);
NavigationUI.setupWithNavController(navView, navController);
}
}
implementation 'com.github.bumptech.glide:glide:4.15.1'
右键->新建->activity->TabbedActivity
在你的ui包中会多出三个java类,res/layout中多出两个tab相关的布局文件
SectionsPagerAdapter类:
页面适配器,调整页面的标题,个数等.请根据自身要求和 本文档代码简化自动生成的代码
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
/**
* A [FragmentPagerAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private String[] tabs = new String[]{"精选","热门","推荐"};
/**
* 构造函数
* @param context 上下文
* @param fm 片段
*/
public SectionsPagerAdapter(Context context, FragmentManager fm) {
super(fm);
}
/**
* 获取对应位置的fragment
* @param position fragment下标
* @return 对应的fragment
*/
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a TabFragment (defined as a static inner class below).
return TabFragment.newInstance(position + 1);
}
/**
* 获取页面的标题
* @param position 标题下标
* @return 页面标题
*/
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return tabs[position];
}
/**
* 获取页面个数
* @return 页面个数
*/
@Override
public int getCount() {
// Show 2 total pages.
return tabs.length;
}
}
TabFragment类:
对fragment进行配置,无论有多少个标题和页面,fragment都只有一个, 每个页 面大体相同,不同的是数据,将数据进行替换就可以达到不同页面的效果(这种替换方法 只适合比较简单的分类等功能)
首先先将该类简化到如下,后续涉及页面代码时再回来补全
private static final String ARG_SECTION_NUMBER="section_number";
/*
创建实例的函数
@param index 要展示的页面的下标
* @return fragment片段
*/
public static TabFragment newInstance(int index){
TabFragment fragment=new TabFragment();
Bundle bundle=new Bundle();
bundle.putInt(ARG_SECTION_NUMBER,index);
fragment.setArguments(bundle);
return fragment;
}
inflater它的作用是将一个layout.xml布局文件变为一个View对象。 尤其在ListView、 GridView、RecyclerView的Adapter还有组合自定义控件中,我们都会使 用 inflate () 方法去加载一个布局,作为每个Item的布局。
Activity类:简单简化即可
import android.os.Bundle;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.example.demo1.ui.main.SectionsPagerAdapter;
import com.example.demo1.databinding.ActivityTabBinding;
public class TabActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tab);
SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(this, getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.view_pager);
viewPager.setAdapter(sectionsPagerAdapter);
TabLayout tabs = findViewById(R.id.tabs);
tabs.setupWithViewPager(viewPager);
}
}
fragment_tab.xml:
片段布局,即是每个不同页面的大致布局。 因为是仿照tx视频的分类页面,因此本布局只需要一个RecyclerView控件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.TabFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_tab.xml:
Tab的大体布局,请注意app:tabMode="scrollable"属性,若是选择了fixed, 则全部title将平分屏幕的空间,不加该属性默认自动,当选项卡数量较少且可以适应屏幕宽度时, 将使用固定模式;当选项卡数量较多或无法适应屏幕宽度时,将使用可滚动模式。
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".TabActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"
tools:ignore="SpeakableTextPresentCheck" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:ignore="SpeakableTextPresentCheck" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
本部分与第三方库使用RecyclerView几乎一样,所以会快速带过
创建item_video.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_margin="10dp"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/img_video"
tools:src="@drawable/avatar"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="120dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/tv_video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@string/user_name"
android:gravity="center"/>
</LinearLayout>
预期效果图如下:
创建Video实体类:
public class Video {
private String name;
private String imgUrl;
public Video(String name, String imgUrl) {
this.name = name;
this.imgUrl = imgUrl;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getImgUrl() {
return imgUrl;
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
}
}
请注意,这里的图片使用的String类型,使用网络图片的url地址作为资源。 安卓开发不能像html一样直接使用网络引用url,必须先添加网络权限。
添加网络权限:
在AndroidManifest.xml的manifest标签下添加
<uses-permission
android:name="android.permission.INTERNET"
tools:ignore="ManifestOrder" />
创建VideoAdapter适配器:
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.viewholder.BaseViewHolder;
import com.example.demo1.R;
import com.example.demo1.entity.Song;
public class VideoAdapter extends BaseQuickAdapter<Video, BaseViewHolder> {
public VideoAdapter(int layoutResId) {
super(layoutResId);
}
@Override
protected void convert(@NonNull BaseViewHolder holder, Video song) {
holder.setText(R.id.tv_video_name, song.getName());
ImageView imageView = holder.getView(R.id.img_video);
Glide.with(getContext()).load(song.getImgUrl()).into(imageView);
}
}
在TabFragment中初始化:
创建页面
/**
*将布局转化为一个view对象
* @param inflater 用于将布局转化为view对象
* @param container
* 如果非 null,则这是片段的 UI 应附加到的父视图。 片段不应添加视图本身,但这可用于生成视图的 LayoutParams。
* @param savedInstanceState 如果非 null,则此片段将从先前保存的状态重新构造,如此处给出。
* @return view对象
*/
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return LayoutInflater.from(getContext()).inflate(R.layout.fragment_tab, container, false);
}
初始化内容:
/**
* 创建视图完成后的初始化工作
* @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
* @param savedInstanceState 如果非 null,则此片段将从先前保存的状态重新构造,如此处给出。
*/
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
// 设置网格状,因为默认是纵向滚动,所以2代表2列网格
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 2);
recyclerView.setLayoutManager(layoutManager);
adapter = new VideoAdapter(R.layout.item_video);
recyclerView.setAdapter(adapter);
loadData();
}
/**
* 加载数据
*/
private void loadData() {
List<Video> songs = new ArrayList<>();
Video song = new Video("电影1", "https://puui.qpic.cn/vcover_vt_pic/0/mzc0020050ixd7l1677735885503/350?max_age=7776001");
Video video1 = new Video("电影2", "https://puui.qpic.cn/vcover_vt_pic/0/mzc002009nbjfwn1657179757077/350?max_age=7776001");
Video video2 = new Video("电影3", "https://puui.qpic.cn/vcover_vt_pic/0/mzc00200qwmqz011684115814211/350?max_age=7776001");
Video video3 = new Video("电影4", "https://puui.qpic.cn/vcover_vt_pic/0/mzc002000umo4761658894202518/350?max_age=7776001");
Video video4 = new Video("电影5", "https://puui.qpic.cn/vcover_vt_pic/0/nbusftujqwczi7y1618415148198/350?max_age=7776001");
Video video5 = new Video("电影6", "https://puui.qpic.cn/vcover_vt_pic/0/mzc00200zlh907s1660549999040/350?max_age=7776001");
Video video6 = new Video("电影7", "https://puui.qpic.cn/vcover_vt_pic/0/z6j3ixjjcokafyc1566896570/350?max_age=7776001");
songs.add(song);
songs.add(video1);
songs.add(video2);
songs.add(video3);
songs.add(video4);
songs.add(video5);
songs.add(video6);
adapter.setList(songs);
}
retrofit
是现在比较流行的网络请求框架,可以理解为okhttp的加强版,底层封装了Okhttp
。
准确来说,Retrofit是一个RESTful的http网络请求框架的封装。
因为网络请求工作本质上是由okhttp来完成,而Retrofit负责网络请求接口的封装。
本质过程:App应用程序通过Retrofit
请求网络,实质上是使用Retrofit
接口层封装请求参数、
Header、Url等信息,之后由okhttp来完成后续的请求工作。在服务端返回数据后,okhttp将原始数据交给Retrofit,Retrofit根据用户需求解析。
Retrofit使用大量注解来简化请求,Retrofit将okhttp请求抽象成java接口,使用注解来配置和描述网络请求参数。大概可以分为以下几类,我们先来看看各个注解的含义,再一一去实践解释。
请求方法注解 | 说明 |
---|---|
@GET | get请求 |
@POST | post请求 |
@PUT | put请求 |
@DELETE | delete请求 |
@PATCH | patch请求,该请求是对put请求的补充,用于更新局部资源 |
@HEAD | head请求 |
@OPTIONS | options请求 |
@HTTP | 通过注解,可以替换以上所有的注解,它拥有三个属性:method、path、hasBody |
请求头注解 | 说明 |
---|---|
@Headers | 用于添加固定请求头,可以同时添加多个,通过该注解的请求头不会相互覆盖,而是共同存在 |
@Header | 作为方法的参数传入,用于添加不固定的header,它会更新已有请求头 |
请求参数注解 | 说明 |
---|---|
@Body | 多用于Post请求发送非表达数据,根据转换方式将实例对象转化为对应字符串传递参数,比如使用Post发送Json数据,添加GsonConverterFactory则是将body转化为json字符串进行传递 |
@Filed | 多用于Post方式传递参数,需要结合@FromUrlEncoded使用,即以表单的形式传递参数 |
@FiledMap | 多用于Post请求中的表单字段,需要结合@FromUrlEncoded使用 |
@Part | 用于表单字段,Part和PartMap与@multipart注解结合使用,适合文件上传的情况 |
@PartMap | 用于表单字段,默认接受类型是Map<String,RequestBody>,可用于实现多文件上传 |
@Path | 用于Url中的占位符 |
@Query | 用于Get请求中的参数 |
@QueryMap | 与Query类似,用于不确定表单参数 |
@Url | 指定请求路径 |
标记类注解 | 说明 |
---|---|
@FromUrlCoded | 表示请求发送编码表单数据,每个键值对需要使用@Filed注解 |
@Multipart | 表示请求发送form_encoded数据(使用于有文件上传的场景),每个键值对需要用@Part来注解键名,随后的对象需要提供值 |
@Streaming | 表示响应用字节流的形式返回,如果没有使用注解,默认会把数据全部载入到内存中,该注解在下载大文件时特别有用 |
我们先来解释注解的使用和需要注意的问题(具体的使用步骤后面会给出)。上面提到注解是用来配置和描述网络请求参数的,我们来逐个讲解一下,首先创建网络接口类:
public interface Api {
//get请求
@GET("user")
Call<ResponseBody> getData();
}
上面的网络接口最简单的一种形式,我们先从简单的开始,一步步深入了解。这是一个没有网络参数的get请求方式,需要在方法头部添加@GET注解,表示采用get方法访问网络请求,括号内的是请求的地址(Url的一部分) ,其中返回类型是Call<*>,*表示接收数据的类,如果想直接获取ResponseBody中的内容,可以定义网络请求返回值为Call<ResponseBody>
,ResponseBody是请求网络后返回的原始数据,如果网络请求没有参数,不用写。
这里特别说明Url的组成(下面会讲解到),retrofit把网络请求的Url分成两部分设置:第一部分在创建Retrofit实例时通过.baseUrl()设置,第二部分在网络接口注解中设置,比如上面接口的"/user",网络请求的完整地址Url = Retrofit实例.baseUrl()
+网络请求接口注解()
。
下面我们来看看有参数的get请求方法:
@GET("user")
Call<ResponseBody> getData2(@Query("id") long idLon, @Query("name") String nameStr);
添加参数在方法括号内添加@Query,后面是参数类型和参数字段,表示后面idLon的取值作为"id"的值,nameStr的取值作为"name"的值,其实就是键值对,Retrofit会把两个字段拼接到接口中,追加到"/user"后面。比如:baseUrl为 api.github.com/ ,那么拼接网络接口注解中的地址后变为:api.github.com/user ,我们需要传入的id=10006,name="刘亦菲",那么拼接参数后就是完整的请求地址:https://api.github.com/user?id=10006&name=刘亦菲
。
@GET("user")
Call<ResponseBody> getData3(@QueryMap Map<String, Object> map);
Map<String, Object> map = new HashMap<>();
map.put("id", 10006);
map.put("name", "刘亦菲");
Call<ResponseBody> call = retrofit.create(Api.class).getData3(map);
我们来看看没有参数的post方法的请求:
@POST("user/emails")
Call<ResponseBody> getPostData();
post请求网络方法,需要在方法头部添加@POST注解,表示采用post方法访问网络请求,这里是没有参数的,那有参数的Post请求方法该怎样写呢?
@FormUrlEncoded
@POST("user/emails")
Call<ResponseBody> getPostData2(@Field("name") String nameStr, @Field("sex") String sexStr);
Post请求如果有参数需要在头部添加@FormUrlEncoded注解,表示请求实体是一个From表单,每个键值对需要使用@Field注解,使用@Field添加参数,这是发送Post请求时,提交请求的表单字段,必须要添加的,而且需要配合@FormUrlEncoded使用。
@FormUrlEncoded
@POST("user/emails")
Call<ResponseBody> getPsotData3(@FieldMap Map<String, Object> map);
Map<String, Object> fieldMap = new HashMap<>();
map.put("name", "刘亦菲");
map.put("sex", "女");
Call<ResponseBody> psotData3 = retrofit.create(Api.class).getPostData3(map);
当有多个不确定参数时,我们可以使用@FieldMap注解,@FieldMap与@Field的作用一致,可以用于添加多个不确定的参,类似@QueryMap,Map的key作为表单的键,Map的value作为表单的值。
适用于Post请求的还有一个注解@Body,@Body可以传递自定义类型数据给服务器,多用于post请求发送非表单数据,比如用传递Json格式数据,它可以注解很多东西,比如HashMap、实体类等,我们来看看它用法:
@POST("user/emails")
Call<ResponseBody> getPsotDataBody(@Body RequestBody body);
特别注意:@Body注解不能用于表单或者支持文件上传的表单的编码,即不能与@FormUrlEncoded和@Multipart注解同时使用,否则会报错。
我们找到@Body的源码发现,isFormEncoded为true或者isMultipart为ture时,满足其中任何一个条件都会抛出该异常,通过变量名字可以看出使用@ FormUrlEncoded 或者@Multipart 标签。
if (annotation instanceof Body) {
if (isFormEncoded || isMultipart) {
throw parameterError(method, p,
"@Body parameters cannot be used with form or multi-part encoding.");
}
}
@HTTP注解的作用是替换@GET、@POST、@PUT、@DELETE、@HEAD以及更多拓展功能
@HTTP(method = "GET", path = "user/keys", hasBody = false)
Call<ResponseBody> getHttpData();
@HTTP注解可以通过method字段灵活设置具体请求方法,通过path设置网络请求地址,用的比较少。@HTTP替换@POST、@PUT、@DELETE、@HEAD等方法的用法类似,这里就不一一讲解了。
@GET("orgs/{id}")
Call<ResponseBody> getPathData(@Query("name") String nameStr, @Path("id") long idLon);
@Path注解用于Url中的占位符{}
,所有在网址中的参数,如上面 @GET("orgs/{id}")
的id,通过{}
占位符来标记id,使用@Path注解传入idLon的值,注意有的Url既有占位符又有"?"后面的键值对,其实@Query和@Path两者是可以共用的。在发起请求时,{id}会被替换为方法中第二个参数的值idLon。
如果需要重新地址接口地址,可以使用@Url,将地址以参数的形式传入即可。如果有@Url注解时,GET传入的Url可以省略。
@GET
Call<ResponseBody> getUrlData(@Url String nameStr, @Query("id") long idLon);
我们可以在方法参数内添加请求头,@Header用于添加不固定的请求头,作用于方法的参数,作为方法的参数传入,该注解会更新已有的请求头。
@GET("user/emails")
Call<ResponseBody> getHeaderData(@Header("token") String token);
@header 请求头注解,用于添加不固定请求头 我们想对某个方法添加固定请求头时可以参考下面的写法,@headers用于添加固定的请求头,作用于方法,可以同时添加多个,通过该注解添加的请求头不会相互覆盖,而是共同存在。
@Headers({"phone-type:android", "version:1.1.1"})
@GET("user/emails")
Call<ResponseBody> getHeadersData();
@Streaming
@POST("gists/public")
Call<ResponseBody> getStreamingBig();
@Multipart
@POST("user/followers")
Call<ResponseBody> getPartData(@Part("name") RequestBody name, @Part MultipartBody.Part file);
上面的使用是一个上传文字和文件的写法,在使用@Part注解时需要在头部添加@Multipart注解,实现支持文件上传,我们来看看上面代码怎么使用:
//声明类型,这里是文字类型
MediaType textType = MediaType.parse("text/plain");
//根据声明的类型创建RequestBody,就是转化为RequestBody对象
RequestBody name = RequestBody.create(textType, "这里是你需要写入的文本:刘亦菲");
//创建文件,这里演示图片上传
File file = new File("文件路径");
if (!file.exists()) {
file.mkdir();
}
//将文件转化为RequestBody对象
//需要在表单中进行文件上传时,就需要使用该格式:multipart/form-data
RequestBody imgBody = RequestBody.create(MediaType.parse("image/png"), file);
//将文件转化为MultipartBody.Part
//第一个参数:上传文件的key;第二个参数:文件名;第三个参数:RequestBody对象
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), imgBody);
Call<ResponseBody> partDataCall = retrofit.create(Api.class).getPartData(name, filePart);
首先声明类型,然后根据类型转化为RequestBody对象,返回RequestBody或者转化为 MultipartBody.Part
,需要在表单中进行文件上传时,就需要使用该格式:multipart/form-data
。
@PartMap的使用与@FieldMap和@QueryMap的使用类似,用于多文件上传,我们直接看代码:
@Multipart
@POST("user/followers")
Call<ResponseBody> getPartMapData(@PartMap Map<String, MultipartBody.Part> map);
File file1 = new File("文件路径");
File file2 = new File("文件路径");
if (!file1.exists()) {
file1.mkdir();
}
if (!file2.exists()) {
file2.mkdir();
}
RequestBody requestBody1 = RequestBody.create(MediaType.parse("image/png"), file1);
RequestBody requestBody2 = RequestBody.create(MediaType.parse("image/png"), file2);
MultipartBody.Part filePart1 = MultipartBody.Part.createFormData("file1", file1.getName(), requestBody1);
MultipartBody.Part filePart2 = MultipartBody.Part.createFormData("file2", file2.getName(), requestBody2);
Map<String,MultipartBody.Part> mapPart = new HashMap<>();
mapPart.put("file1",filePart1);
mapPart.put("file2",filePart2);
Call<ResponseBody> partMapDataCall = retrofit.create(Api.class).getPartMapData(mapPart);
上面的(@PartMap Map<String, MultipartBody.Part> map)
方法参数中的 MultipartBody.Part 可以是RequestBody、String、MultipartBody.Part等类型,可以根据个人需要更换,这里就不一一说明了。
retrofit是网络请求框架,毫无疑问必须要在AndroidManifest.xml文件中添加网络权限
<uses-permission android:name="android.permission.INTERNET"/>
在app目录下的build.gradle文件中添加Retrofit依赖库,由于Retrofit已经封装有okhttp所以不需要再添加okhttp的库
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
在正式的请求网络数据中,返回的数据的外嵌套部分都是一样的,携带状态码、状态信息、实体数据一起返回,我们可以将它封装一个统一的数据回调类。
public class Data<T> {
private int code;
private String message;
private T data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.example.demo1.entity;
public class Video {
private String name;
private String picurl;
public Video(String name, String picurl) {
this.name = name;
this.picurl = picurl;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPicurl() {
return picurl;
}
public void setPicurl(String picurl) {
this.picurl = picurl;
}
}
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.HTTP;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface Api {
//get请求
@GET("api/rand.music")
Call<Data<Video>> getJsonData(@Query("sort") String sort, @Query("format") String format);
//接口的参数应与后端保持一直,包括参数名称
}
在方法头部添加@GET注解,表示采用get方法访问网络请求,括号内的是请求的地址(Url的一部分) ,其中返回类型是Call<Video>,Video表示接收数据的类,通常是自定义的类,Data是上面封装的一个接收数据的统一公共类,添加参数在方法括号内添加@Query,后面是参数类型和参数字段,其实就是键值对的形式。
在任意Activity或者fragment中实例化:
private Retrofit retrofit;
private Api api;
public void initRequest() {
retrofit = new Retrofit.Builder()
//设置网络请求BaseUrl地址
.baseUrl("https://api.uomg.com/")
//设置数据解析器
.addConverterFactory(GsonConverterFactory.create())
.build();
//创建网络请求接口对象实例
api = retrofit.create(Api.class);
}
此处我在fragment中使用一个方法封装初始化请求部分retrofit
和api
都是全局变量,通过new Retrofit.Builder().buile()
构建Retrofit实例对象,同时设置baseUrl地址,Retrofit把网络请求的URL 分成了两部分设置: 第一部分:在创建Retrofit实例时通过.baseUrl()
设置,baseUrl地址必须以/
结尾,否则会报错。
第二部分:在网络请求接口的注解设置,就是在上面的APi接口中用GET注解的字符串:
@GET("api/rand.music")
上面地址拼接后的完整地址为:httpsf://api.uomg.com/api/rand.music?srot=新歌榜&format=json,然后通过addConverterFactory设置数据解释器,这里添加的是Gson解释器。这是为了使来自接口的json结果会自动解析成定义好的字段和类型都相符的json对象接受类,在Retrofit 2.0中已经没有Converter,需要自己创建一个Converter, 不然Retrofit只能接收字符串结果,最后需要自己去解析。
在app目录下的build.gradle中添加Converter依赖:
implementation 'com.squareup.retrofit2:converter-gson:2.0.2'
Retrofit支持多种数据解析,需要在Gradle添加依赖:
数据解析器 | Gradle依赖 |
---|---|
Gson | com.squareup.retrofit2:converter-gson:2.0.2 |
Jackson | com.squareup.retrofit2:converter-jackson:2.0.2 |
Simple XML | com.squareup.retrofit2:converter-simplexml:2.0.2 |
Protobuf | com.squareup.retrofit2:converter-protobuf:2.0.2 |
Moshi | com.squareup.retrofit2:converter-moshi:2.0.2 |
Wire | com.squareup.retrofit2:converter-wire:2.0.2 |
Scalars | com.squareup.retrofit2:converter-scalars:2.0.2 |
public void doNetwork(View view) { TextView buttonBack1 = view.findViewById(R.id.text_dashboard);
buttonBack1.setOnClickListener(view1 -> {
Toast.makeText(getContext(), "点击了按钮!", Toast.LENGTH_SHORT).show();
//对发送请求进行封装
Call<Data<Video>> dataCall = api.getJsonData("新歌榜", "json");
}
调用Retrofit实例对象 create()
方法,传入网络接口类,得到接口对象实例,调用接口类中的方法。
public void doNetwork(View view) { TextView buttonBack1 = view.findViewById(R.id.text_dashboard);
buttonBack1.setOnClickListener(view1 -> {
Toast.makeText(getContext(), "点击了按钮!", Toast.LENGTH_SHORT).show();
//对发送请求进行封装
Call<Data<Video>> dataCall = api.getJsonData("新歌榜", "json");
//异步请求
dataCall.enqueue(new Callback<Data<Video>>() {
//请求成功回调
@Override
public void onResponse(Call<Data<Video>> call, Response<Data<Video>> response) {
}
//请求失败回调
@Override
public void onFailure(Call<Data<Video>> call, Throwable t) {
}
});
}
/**
* 定义网络请求按钮
* @param view fragment页面
*/
public void doNetwork(View view) {
TextView buttonBack1 = view.findViewById(R.id.text_dashboard);
buttonBack1.setOnClickListener(view1 -> {
Toast.makeText(getContext(), "点击了按钮!", Toast.LENGTH_SHORT).show();
//对发送请求进行封装
Call<Data<Video>> dataCall = api.getJsonData("新歌榜", "json");
//处理请求数据
dataCall.enqueue(new Callback<Data<Video>>() {
@SuppressLint("SetTextI18n")
@Override
public void onResponse(Call<Data<Video>> call, Response<Data<Video>> response) {
Data<Video> body = response.body();
if (body == null) return;
Video song = body.getData();
if (song == null) return;
TextView tv_username = view.findViewById(R.id.tv_username);
tv_username.setText(song.getName());
ImageView img = view.findViewById(R.id.img_video_test);
if (song.getPicurl() == null)
img.setImageResource(R.drawable.avatar);
else {
Glide.with(Objects.requireNonNull(getContext())).load(song.getPicurl()).into(img);
}
Toast.makeText(getContext(), "get回调成功:异步执行", Toast.LENGTH_SHORT).show();
}
@SuppressLint("RestrictedApi")
@Override
public void onFailure(Call<Data<Video>> call, Throwable t) {
Log.e(TAG, "回调失败:" + t.getMessage() + "," + t);
Toast.makeText(getContext(), "回调失败", Toast.LENGTH_SHORT).show();
}
});
});
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRequest();
doNetwork(view);
}
这几步代码是一点一点补全的,且均在同一个fragment类中
点击前
点击后:
成功获取了网络资源!!!!!
流程与之前的get请求相似,故省略一些相同的步骤,此处的实体类Order请替换为自己的
@HTTP(method = "POST", path = "order/create",hasBody = true)
Call<Data<Order>> createOrder(@Body Order order);
public void initRequest() {
retrofit = new Retrofit.Builder()
//设置网络请求BaseUrl地址
.baseUrl("http://192.168.43.176:9000/")
//设置数据解析器
.addConverterFactory(GsonConverterFactory.create())
.build();
//创建网络请求接口对象实例
api = retrofit.create(Api.class);
}
在学习阶段,baseurl请使用主机的IP地址而不是localhost,并且保持手机电脑处于同同一网络环境
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
//初始化请求
initRequest();
//初始化下单事件
doCreateOrder(view);
}
/**
* 下单按钮
* @param view 下单页面
*/
public void doCreateOrder(View view) {
TextView buttonBack1 = view.findViewById(R.id.text_notifications);
buttonBack1.setOnClickListener(view1 -> {
Toast.makeText(getContext(), "点击了按钮!", Toast.LENGTH_SHORT).show();
//对发送请求进行封装
Call<Data<Order>> dataCall = api.createOrder(new Order(1, 1, 2, 114514));
//处理请求数据
dataCall.enqueue(new Callback<Data<Order>>() {
@SuppressLint("SetTextI18n")
@Override
public void onResponse(Call<Data<Order>> call, Response<Data<Order>> response) {
Data<Order> body = response.body();
if (body == null) return;
Order order = body.getData();
if (order == null) return;
TextView tv_userId = view.findViewById(R.id.tv_order_userId);
tv_userId.setText(Integer.toString(order.getUserId()));
TextView tv_productId = view.findViewById(R.id.tv_order_productId);
tv_productId.setText(Integer.toString(order.getProductId()));
TextView money = view.findViewById(R.id.tv_order_money);
money.setText(Integer.toString(order.getMoney()));
TextView count = view.findViewById(R.id.tv_order_count);
count.setText(Integer.toString(order.getCount()));
Toast.makeText(getContext(), "创建订单成功:异步执行", Toast.LENGTH_SHORT).show();
}
@SuppressLint("RestrictedApi")
@Override
public void onFailure(Call<Data<Order>> call, Throwable t) {
Log.e(TAG, "回调失败:" + t.getMessage() + "," + t);
Toast.makeText(getContext(), "回调失败", Toast.LENGTH_SHORT).show();
}
});
});
}
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
OrderService orderService;
@Resource
StorageFeign storageFeign;
@Resource
OrderTccService orderTccService;
@PostMapping("/create")
@GlobalTransactional
public JSONObject orderAdd(@RequestBody Order order) {
//orderService.save(order);
orderTccService.create(order);
storageFeign.deduction(order.getProductId(),order.getCount());
Result<Order> json = new Result<>();
json.put("code", 200);
json.put("message", "请求数据成功");
json.put("data", order);
return json;
}
}
此处为只粘贴了Controller部分,至于service及其相关的内容不属于本课程,不过多展示。
控制器接收一个order对象,但已经被前端@Body注解转化为json格式,后端完成服务后,向前端返回了该订单,
返回的而数据依然是一个json格式的对象,Result<T>与前端的Data<T>一样。
添加依赖:
implementation "androidx.room:room-runtime:2.4.0" , annotationProcessor "androidx.room:room-compiler:2.4.0"
添加对存储的读写权限:
<!-- 读写手机存储权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
创建一个实体类(Entity):实体类表示数据库中的表,并定义表的结构。可以使用@Entity注解标记类,并使用@PrimaryKey注解标记主键字段。还可以使用其他注解如@ColumnInfo、@Ignore等来定义列信息和忽略字段。
@Entity(tableName = "users")
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "name")
public String name;
}
创建一个访问对象(DAO):DAO(Data Access Object)是用于访问数据库的接口或抽象类,定义了对数据库进行CRUD操作的方法。可以使用@Dao注解标记接口,并在方法上使用@Insert、@Update、@Delete等注解来指定对应的操作。
@Dao
public interface UserDao {
@Insert
void insert(User user);
@Update
void update(User user);
@Delete
void delete(User user);
}
创建数据库对象(Database):数据库对象是Room的核心部分,用于定义数据库的配置和操作方法。需要继承RoomDatabase类, 并使用@Database注解标记类,并指定包含的实体类和版本号。
由于数据库创建对资源消耗非常大,故使用单例模式来限制其声明。
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase mAppDatabase;
//单例设计模式
public static AppDatabase getInstance(Context context) {
if (mAppDatabase == null) {
synchronized (AppDatabase.class) {
if (mAppDatabase == null) {
mAppDatabase = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "dbRoomTest.db")
.addMigrations()
// 默认不允许在主线程中连接数据库
// .allowMainThreadQueries()
.build();
}
}
}
return mAppDatabase;
}
public abstract UserDao userDao();
}
初始化数据库:通常在Application类中进行数据库的初始化,通过调用Room.databaseBuilder()方法来创建数据库实例。
//全局初始化room
AppDatabase db = AppDatabase.getInstance(this);
UserDao userDao = db.userDao();
//执行数据库操作:通过获取数据库对象和DAO对象,可以进行数据库的操作,如插入数据、查询数据等。
User user = new User();
user.id = 1;
user.name = "John";
userDao().insert(user);
//这部分请使用新线程执行
这样就可以向Room数据库添加数据了。请注意,在主线程中执行数据库操作可能会导致UI卡顿,因此推荐使用异步操作,如使用AsyncTask、LiveData、RxJava等来处理数据库操作。
此外,还可以通过使用事务(Transaction)来执行一系列的数据库操作,保证数据的一致性和完整性。
以上是使用Room添加数据的基本流程,具体的实现可能会根据项目的需求和结构有所变化。建议参考Room的官方文档和示例代码,以了解更多关于Room数据库的使用方法。
引入依赖包:implementation 'com.zhihu.android:matisse:0.5.3-beta3'
,//动态权限implementation 'pub.devrel:easypermissions:2.0.1
public void initImageSelected(){
Matisse.from(this).choose(MimeType.ofImage(), false)//此处选择了图片文件
.countable(true)
.maxSelectable(1) //最大选择数量
.addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))//过滤器
.gridExpectedSize((int) getResources().getDimension(R.dimen.imageSelectDimen))
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
.thumbnailScale(0.87f) //清晰度,略缩图
.imageEngine(new GlideEngine()) //加载图片引擎
.forResult(REQUEST_CODE_CHOOSE);//设置请求码
}
/**
* 设置选择的图片数据,并做权限验证再进入相册
*/
private void setPicData() {
// 有权限,设置imageSelected
if (EasyPermissions.hasPermissions(this,permissions)){
initImageSelected();
}else {
// 无权限, 请求权限
EasyPermissions.requestPermissions(this, "请求必要的权限,拒绝权限可能会无法使用!", REQUEST_CODE_CHOOSE, permissions);
}
}
既然都请求了权限,那当然有必要赋予app相关的权限,在清单文件中赋予权限:
<!--访问手机照片权限-->
<uses-permission android:name="android.permission.CAMERA" />
有了授权,就需要去确认是否授权成功,使用以下回调类,结果会自动返回setPicData
:
/**
* 成功获取权限
* @param requestCode 请求码
* @param perms ?
*/
@Override
public void onPermissionsGranted(int requestCode, @NonNull List<String> perms) {
Log.e(TAG,perms.toString());
initImageSelected();
}
/**
* 获取权限失败
* @param requestCode 请求码
* @param perms ?
*/
@Override
public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
Log.e(TAG,"权限请求失败");
//拒绝权限且点击了不再提示
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
//跳转应用设置页面
new AppSettingsDialog.Builder(this).build().show();
} else {
EasyPermissions.requestPermissions(this, "请求必要的权限,拒绝权限可能会无法使用!", 1, perms.toString());
}
}
效果类似于下级向上级传递参数:
/**
* 接收matisse返回的图片
* @param requestCode 请求码
* @param resultCode 返回码
* @param data 返回的图片
*/
@SuppressLint("CheckResult")
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
CircleImageView imageView = findViewById(R.id.profile_avatar_view);
if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) {
//图片路径 同样视频地址也是这个
List<String> pathList = Matisse.obtainPathResult(data);
//Uri 格式的
List<Uri> pathList1 = Matisse.obtainResult(data);
for (int i = 0; i < pathList.size(); i++) {
Log.i("图片" + (i + 1) + "地址", pathList.get(i));
uploadFile(pathList.get(i)); //自定义的文件上传方式
}
uri = pathList1.get(0);
if (uri != null) { //将图片跟新到头像当中
Glide.with(ProfileActivity.this)
.asBitmap() // some .jpeg files are actually gif
.load(uri)
.apply(new RequestOptions() {{
override(Target.SIZE_ORIGINAL);
}})
.into(imageView);
}
else {
Toast.makeText(ProfileActivity.this, "没找到图片", Toast.LENGTH_SHORT).show();
}
}
}
自此,Matisse打开相册功能示例结束。
<VideoView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="200dp"
android:foregroundGravity="center" />
1、首先声明两个对象:视频播放器和播放媒体控制器
//视频播放器
private VideoView video_player;
//播放媒体控制器
private MediaController mMediaController;
2、在后端返回数据的地方填入视频地址,sorts是后端返回的视频列表
sortAdapter1.setList(sorts);
sortAdapter1.setOnItemClickListener((sortAdapter, view, position) -> {
String uri = videos.getVideo().get(position).getVideoUrl();
//String uri2 = "http://192.168.43.176:9200/file/videoFile/摇曳露营-0.mp4"; //网络
video_player.setVideoURI(Uri.parse(uri)); //设置网络视频的地址
mMediaController.setMediaPlayer(video_player);// 为控制器指定播放器
mMediaController.setVisibility(View.GONE);//我设置了控制器隐藏,其本身ui略丑
video_player.setMediaController(mMediaController);// 为播放器指定控制器
video_player.start();
});
添加依赖:implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
SwipeRefreshLayout内只能有一个子控件并且必须具有滑动功能。
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/msg_swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="70dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
private void handleDownPullUpdate() {
//设置刷新时小圆圈的颜色变化
swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary, R.color.colorSky);
swipeRefreshLayout.setOnRefreshListener(() -> {
//被刷新时的操作
//更新UI
loadMessageData(user.getId());
new Handler().postDelayed(() -> {
//更新成功后设置UI,停止更新
Toast.makeText(getContext(), "刷新成功!", Toast.LENGTH_SHORT).show();
swipeRefreshLayout.setRefreshing(false);
}, 2000);//理论时间应该是等待请求完成
});
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。