Android launcher3实现简单的负一屏功能

1.前言:
之前实现过Launcher3从凑提修改成单层,今天来讲解一下如何实现一个简单的负一屏功能,涉及的类如下,直接看代码。
2.NegativeScreenAdapter:
kotlin
package com.example.negativescreendemo.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.negativescreendemo.ActionItem;
import com.example.negativescreendemo.CardItem;
import com.example.negativescreendemo.NewsItem;
import com.example.negativescreendemo.R;
import com.example.negativescreendemo.ScheduleItem;
import com.example.negativescreendemo.TodoItem;
import java.util.List;
/**
* @author: njb
* @date: 2025/8/20 21:19
* @desc: 描述
*/
public class NegativeScreenAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
private Context context;
private List<CardItem> cardItems;
private OnItemClickListener listener;
// 卡片类型
public enum CardType {
ACTIONS, SCHEDULE, NEWS, TODO
}
public NegativeScreenAdapter(Context context, List<CardItem> cardItems) {
this.context = context;
this.cardItems = cardItems;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
switch (viewType) {
case 0: // 快捷功能卡片
View actionView = inflater.inflate(R.layout.item_action_card, parent, false);
return new ActionCardViewHolder(actionView);
case 1: // 日程卡片
View scheduleView = inflater.inflate(R.layout.item_schedule_card, parent, false);
return new ScheduleCardViewHolder(scheduleView);
case 2: // 新闻卡片
View newsView = inflater.inflate(R.layout.item_news_card, parent, false);
return new NewsCardViewHolder(newsView);
case 3: // 待办事项卡片
View todoView = inflater.inflate(R.layout.item_todo_card, parent, false);
return new TodoCardViewHolder(todoView);
default:
return null;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
CardItem item = cardItems.get(position);
if (holder instanceof ActionCardViewHolder) {
((ActionCardViewHolder) holder).title.setText(item.getTitle());
ActionAdapter actionAdapter = new ActionAdapter(context, item.getActionItems());
((ActionCardViewHolder) holder).recyclerView.setAdapter(actionAdapter);
((ActionCardViewHolder) holder).recyclerView.setLayoutManager(
new GridLayoutManager(context, 3));
}
else if (holder instanceof ScheduleCardViewHolder) {
((ScheduleCardViewHolder) holder).title.setText(item.getTitle());
ScheduleAdapter scheduleAdapter = new ScheduleAdapter(context, item.getScheduleItems());
((ScheduleCardViewHolder) holder).recyclerView.setAdapter(scheduleAdapter);
}
else if (holder instanceof NewsCardViewHolder) {
((NewsCardViewHolder) holder).title.setText(item.getTitle());
NewsAdapter newsAdapter = new NewsAdapter(context, item.getNewsItems());
((NewsCardViewHolder) holder).recyclerView.setAdapter(newsAdapter);
}
else if (holder instanceof TodoCardViewHolder) {
((TodoCardViewHolder) holder).title.setText(item.getTitle());
TodoAdapter todoAdapter = new TodoAdapter(context, item.getTodoItems());
((TodoCardViewHolder) holder).recyclerView.setAdapter(todoAdapter);
}
// 设置点击事件
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onItemClick(position);
}
});
}
@Override
public int getItemCount() {
return cardItems.size();
}
@Override
public int getItemViewType(int position) {
CardType type = cardItems.get(position).getType();
switch (type) {
case ACTIONS: return 0;
case SCHEDULE: return 1;
case NEWS: return 2;
case TODO: return 3;
default: return 0;
}
}
// 快捷功能卡片ViewHolder
public static class ActionCardViewHolder extends RecyclerView.ViewHolder {
TextView title;
RecyclerView recyclerView;
public ActionCardViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.card_title);
recyclerView = itemView.findViewById(R.id.action_recycler);
}
}
// 日程卡片ViewHolder
public static class ScheduleCardViewHolder extends RecyclerView.ViewHolder {
TextView title;
RecyclerView recyclerView;
public ScheduleCardViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.card_title);
recyclerView = itemView.findViewById(R.id.schedule_recycler);
}
}
// 新闻卡片ViewHolder
public static class NewsCardViewHolder extends RecyclerView.ViewHolder {
TextView title;
RecyclerView recyclerView;
public NewsCardViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.card_title);
recyclerView = itemView.findViewById(R.id.news_recycler);
}
}
// 待办事项卡片ViewHolder
public static class TodoCardViewHolder extends RecyclerView.ViewHolder {
TextView title;
RecyclerView recyclerView;
public TodoCardViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.card_title);
recyclerView = itemView.findViewById(R.id.todo_recycler);
}
}
// 快捷功能子项适配器
public class ActionAdapter extends RecyclerView.Adapter<ActionAdapter.ViewHolder> {
private Context context;
private List<ActionItem> items;
public ActionAdapter(Context context, List<ActionItem> items) {
this.context = context;
this.items = items;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context)
.inflate(R.layout.item_action, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ActionItem item = items.get(position);
holder.icon.setImageResource(item.getIconRes());
holder.name.setText(item.getName());
}
@Override
public int getItemCount() {
return items.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
TextView name;
public ViewHolder(View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.action_icon);
name = itemView.findViewById(R.id.action_name);
}
}
}
// 日程子项适配器
public class ScheduleAdapter extends RecyclerView.Adapter<ScheduleAdapter.ViewHolder> {
private Context context;
private List<ScheduleItem> items;
public ScheduleAdapter(Context context, List<ScheduleItem> items) {
this.context = context;
this.items = items;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context)
.inflate(R.layout.item_schedule, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ScheduleItem item = items.get(position);
holder.time.setText(item.getTime());
holder.title.setText(item.getTitle());
holder.location.setText(item.getLocation());
}
@Override
public int getItemCount() {
return items.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView time, title, location;
public ViewHolder(View itemView) {
super(itemView);
time = itemView.findViewById(R.id.schedule_time);
title = itemView.findViewById(R.id.schedule_title);
location = itemView.findViewById(R.id.schedule_location);
}
}
}
// 新闻子项适配器
public class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
private Context context;
private List<NewsItem> items;
public NewsAdapter(Context context, List<NewsItem> items) {
this.context = context;
this.items = items;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context)
.inflate(R.layout.item_news, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
NewsItem item = items.get(position);
holder.title.setText(item.getTitle());
holder.source.setText(item.getSource());
holder.time.setText(item.getTime());
}
@Override
public int getItemCount() {
return items.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView title, source, time;
public ViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.news_title);
source = itemView.findViewById(R.id.news_source);
time = itemView.findViewById(R.id.news_time);
}
}
}
// 待办事项子项适配器
public class TodoAdapter extends RecyclerView.Adapter<TodoAdapter.ViewHolder> {
private Context context;
private List<TodoItem> items;
public TodoAdapter(Context context, List<TodoItem> items) {
this.context = context;
this.items = items;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context)
.inflate(R.layout.item_todo, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TodoItem item = items.get(position);
holder.content.setText(item.getContent());
holder.checkBox.setChecked(item.isCompleted());
holder.checkBox.setOnCheckedChangeListener((buttonView, isChecked) ->
item.setCompleted(isChecked));
}
@Override
public int getItemCount() {
return items.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
CheckBox checkBox;
TextView content;
public ViewHolder(View itemView) {
super(itemView);
checkBox = itemView.findViewById(R.id.todo_checkbox);
content = itemView.findViewById(R.id.todo_content);
}
}
}
// 点击事件接口
public interface OnItemClickListener {
void onItemClick(int position);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
}
3.ActionItem:
kotlin
package com.example.negativescreendemo;
/**
* @author: njb
* @date: 2025/8/20 21:28
* @desc: 描述
*/
public class ActionItem {
private String name;
private int iconRes;
public ActionItem(String name, int iconRes) {
this.name = name;
this.iconRes = iconRes;
}
public String getName() {
return name;
}
public int getIconRes() {
return iconRes;
}
}
4.卡片CardItem:
kotlin
package com.example.negativescreendemo;
import com.example.negativescreendemo.adapter.NegativeScreenAdapter;
import java.util.List;
/**
* @author: njb
* @date: 2025/8/20 21:21
* @desc: 描述
*/
public class CardItem {
private NegativeScreenAdapter.CardType type;
private String title;
private List<ActionItem> actionItems;
private List<ScheduleItem> scheduleItems;
private List<NewsItem> newsItems;
private List<TodoItem> todoItems;
// 私有构造方法,防止直接实例化
private CardItem() {}
/**
* 创建快捷操作卡片
*/
public static CardItem createActionCard(NegativeScreenAdapter.CardType type, String title, List<ActionItem> actionItems) {
CardItem item = new CardItem();
item.type = type;
item.title = title;
item.actionItems = actionItems;
return item;
}
/**
* 创建日程卡片
*/
public static CardItem createScheduleCard(NegativeScreenAdapter.CardType type, String title, List<ScheduleItem> scheduleItems) {
CardItem item = new CardItem();
item.type = type;
item.title = title;
item.scheduleItems = scheduleItems;
return item;
}
/**
* 创建新闻卡片
*/
public static CardItem createNewsCard(NegativeScreenAdapter.CardType type, String title, List<NewsItem> newsItems) {
CardItem item = new CardItem();
item.type = type;
item.title = title;
item.newsItems = newsItems;
return item;
}
/**
* 创建待办事项卡片
*/
public static CardItem createTodoCard(NegativeScreenAdapter.CardType type, String title, List<TodoItem> todoItems) {
CardItem item = new CardItem();
item.type = type;
item.title = title;
item.todoItems = todoItems;
return item;
}
// Getters
public NegativeScreenAdapter.CardType getType() {
return type;
}
public String getTitle() {
return title;
}
public List<ActionItem> getActionItems() {
return actionItems;
}
public List<ScheduleItem> getScheduleItems() {
return scheduleItems;
}
public List<NewsItem> getNewsItems() {
return newsItems;
}
public List<TodoItem> getTodoItems() {
return todoItems;
}
}
5.NewItem:
kotlin
package com.example.negativescreendemo;
/**
* @author: njb
* @date: 2025/8/20 21:28
* @desc: 描述
*/
public class NewsItem {
private String title;
private String source;
private String time;
public NewsItem(String title, String source, String time) {
this.title = title;
this.source = source;
this.time = time;
}
public String getTitle() {
return title;
}
public String getSource() {
return source;
}
public String getTime() {
return time;
}
}
6.ScheduleItem:
kotlin
package com.example.negativescreendemo;
/**
* @author: njb
* @date: 2025/8/20 21:28
* @desc: 描述
*/
public class ScheduleItem {
private String time;
private String title;
private String location;
public ScheduleItem(String time, String title, String location) {
this.time = time;
this.title = title;
this.location = location;
}
public String getTime() {
return time;
}
public String getTitle() {
return title;
}
public String getLocation() {
return location;
}
}
7.TodoItem:
kotlin
package com.example.negativescreendemo;
/**
* @author: njb
* @date: 2025/8/20 21:28
* @desc: 描述
*/
public class TodoItem {
private String content;
private boolean isCompleted;
public TodoItem(String content, boolean isCompleted) {
this.content = content;
this.isCompleted = isCompleted;
}
public String getContent() {
return content;
}
public boolean isCompleted() {
return isCompleted;
}
public void setCompleted(boolean completed) {
isCompleted = completed;
}
}
8.主界面布局:
kotlin
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_50"
tools:context=".NegativeScreenActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 顶部时间天气区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/time_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12:30"
android:textColor="@color/black"
android:textSize="48sp"
android:textStyle="bold" />
<TextView
android:id="@+id/date_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="星期一, 六月 15日"
android:textColor="@color/gray_600"
android:textSize="16sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:layout_gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/weather_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_scan" />
<TextView
android:id="@+id/weather_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="25°C 晴朗"
android:textColor="@color/gray_600"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
<!-- 卡片列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:clipToPadding="false" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
9.主界面测试代码:
kotlin
package com.example.negativescreendemo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.negativescreendemo.adapter.NegativeScreenAdapter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
/**
* @author: njb
* @date: 2025/8/20 21:18
* @desc: 描述
*/
public class NegativeScreenActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private NegativeScreenAdapter adapter;
private List<CardItem> cardItems;
private TextView timeTextView;
private TextView dateTextView;
private TextView weatherTextView;
private ImageView weatherIcon;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_negative_screen);
// 初始化视图
initViews();
// 初始化数据
initData();
// 设置适配器
adapter = new NegativeScreenAdapter(this, cardItems);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 设置点击事件
adapter.setOnItemClickListener(position -> {
CardItem item = cardItems.get(position);
Toast.makeText(NegativeScreenActivity.this,
"点击了: " + item.getTitle(), Toast.LENGTH_SHORT).show();
});
// 更新时间
updateTime();
}
private void initViews() {
recyclerView = findViewById(R.id.recycler_view);
timeTextView = findViewById(R.id.time_text);
dateTextView = findViewById(R.id.date_text);
weatherTextView = findViewById(R.id.weather_text);
weatherIcon = findViewById(R.id.weather_icon);
}
private void initData() {
// 模拟天气数据
weatherTextView.setText("25°C 晴朗");
// 初始化卡片数据
cardItems = new ArrayList<>();
// 添加快捷功能卡片
List<ActionItem> actionItems = new ArrayList<>();
actionItems.add(new ActionItem("扫一扫", R.drawable.ic_scan));
actionItems.add(new ActionItem("付款", R.drawable.ic_pay));
actionItems.add(new ActionItem("乘车码", R.drawable.ic_bus));
actionItems.add(new ActionItem("健康码", R.drawable.ic_health));
actionItems.add(new ActionItem("充电宝", R.drawable.ic_power_bank));
actionItems.add(new ActionItem("更多", R.drawable.ic_more));
cardItems.add(CardItem.createActionCard(NegativeScreenAdapter.CardType.ACTIONS, "快捷功能", actionItems));
// 添加日程卡片
List<ScheduleItem> scheduleItems = new ArrayList<>();
scheduleItems.add(new ScheduleItem("9:30", "团队周会", "会议室A"));
scheduleItems.add(new ScheduleItem("14:00", "客户拜访", "XX公司"));
cardItems.add(CardItem.createScheduleCard(NegativeScreenAdapter.CardType.SCHEDULE, "今日日程", scheduleItems));
// 添加新闻卡片
List<NewsItem> newsItems = new ArrayList<>();
newsItems.add(new NewsItem("最新科技突破:人工智能新进展", "科技日报", "10分钟前"));
newsItems.add(new NewsItem("本地气温将持续升高,最高达35°C", "本地新闻", "30分钟前"));
cardItems.add(CardItem.createNewsCard(NegativeScreenAdapter.CardType.NEWS, "热点新闻", newsItems));
// 添加待办事项卡片
List<TodoItem> todoItems = new ArrayList<>();
todoItems.add(new TodoItem("完成项目报告", false));
todoItems.add(new TodoItem("购买生日礼物", true));
todoItems.add(new TodoItem("回复客户邮件", false));
cardItems.add(CardItem.createTodoCard(NegativeScreenAdapter.CardType.TODO, "待办事项", todoItems));
}
private void updateTime() {
// 更新时间和日期
final Handler handler = new Handler(Looper.getMainLooper());
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
handler.post(() -> {
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
SimpleDateFormat dateFormat = new SimpleDateFormat("EEEE, MMMM d日", Locale.getDefault());
String time = timeFormat.format(new Date());
String date = dateFormat.format(new Date());
timeTextView.setText(time);
dateTextView.setText(date);
});
}
}, 0, 60000); // 每分钟更新一次
}
}
10.实现效果如下:

11.总结:
以分层架构 + 模块化设计 为核心,通过 "数据模型封装 - UI 适配渲染 - 界面逻辑控制" 三层结构,快速搭建 Launcher3 负一屏功能,核心是利用RecyclerView
嵌套实现 "卡片容器 + 子项列表" 的灵活布局,同时保证界面交互与数据展示的解耦。
关键技术点
- RecyclerView 多类型布局 :通过
getItemViewType()
和不同ViewHolder
,实现 4 类卡片的差异化展示,降低代码耦合。 - RecyclerView 嵌套 :外层
RecyclerView
(卡片列表)嵌套内层RecyclerView
(卡片子项),结合GridLayoutManager
(快捷功能 3 列)和LinearLayoutManager
(其他卡片单列),满足不同布局需求。 - 线程安全的 UI 更新 :用
Handler(Looper.getMainLooper())
将Timer
的定时任务切换到主线程,避免子线程操作 UI 的异常。 - 数据模型私有化构造 :
CardItem
通过私有构造 + 静态创建方法,强制规范卡片创建流程,避免错误数据赋值。