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

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

1.前言:

之前实现过Android12Launcher3从抽屉修改成单层和Launcher3显示所有应用列表,今天来讲解一下如何实现一个简单的负一屏功能,直接看代码。

2.NegativeScreenAdapter:

scala 复制代码
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:

arduino 复制代码
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:

typescript 复制代码
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:

typescript 复制代码
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:

typescript 复制代码
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:

typescript 复制代码
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.主界面布局:

ini 复制代码
<?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.主界面测试代码:

java 复制代码
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.源码地址:

gitee.com/jackning_ad...

相关推荐
robotx1 分钟前
安卓线程相关
android
消失的旧时光-194322 分钟前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon1 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon1 小时前
VSYNC 信号完整流程2
android
dalancon1 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013842 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android3 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才4 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶4 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙5 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github