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

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通过私有构造 + 静态创建方法,强制规范卡片创建流程,避免错误数据赋值。

12.源码地址:

https://gitee.com/jackning_admin/negative-screen-demo

相关推荐
coder_pig1 小时前
🤡 公司Android老项目升级踩坑小记
android·flutter·gradle
死就死在补习班2 小时前
Android系统源码分析Input - InputReader读取事件
android
死就死在补习班2 小时前
Android系统源码分析Input - InputChannel通信
android
死就死在补习班2 小时前
Android系统源码分析Input - 设备添加流程
android
死就死在补习班2 小时前
Android系统源码分析Input - 启动流程
android
tom4i3 小时前
Launcher3 to Launchpad 01 布局修改
android
雨白3 小时前
OkHttpClient 核心配置详解
android·okhttp
RabbitYao4 小时前
Android 项目 通过 AndroidStringsTool 更新多语言词条
android·python
RabbitYao4 小时前
使用 Gemini 及 Python 更新 Android 多语言 Excel 文件
android·python