Android App开发实战项目之给用户推荐旅游信息图片(附源码 简单易懂)

news2025/7/8 9:16:26

需要全部源码请点赞关注收藏后评论区留言~~~

一、需求描述

假定用户打开一个旅游App想看看哪里风景比较优美,那么App应当展示各地的风景名声图片,为了让界面不太呆板,可以考虑交错显示风景图片,接着用户向下拉动页面,想要刷新界面浏览更多的图片,此时App界面响应下拉刷新手势弹出加载源泉

等待App努力加载新的图片列表,加载完成之后,界面展示新一批的风景图片,同时加载圆圈消失。

接下来我们实践如何让App从服务端获取随机推荐的风景图片

二、界面设计

界面设计比较简单 主要用到了以下控件

1:循环视图RecyclerView的瀑布流布局

2:下拉刷新布局 SwipeRefreshLayout

界面简单,但是背后设计的网络技术比较复杂 主要用到了以下及几种技术

1:HTTP接口调用 App向后端服务器请求风景图片列表

2:JSON格式 App与服务器之间的数据交互

3:异步任务AsyncTask 访问HTTP接口耗时,需要放在专门的异步任务之中

4:图片加载框架Glide 加载网络图片并显示在界面上  

效果如下 此处建议连接真机测试 模拟机不好与后端网络交互 

 

 

 

 三、关键部分

1:循环视图的首次加载与重新加载

2:原始网络图片的加载

3:不同部分源码之间关系

1:GuessLikeActivity 风景列表的活动代码 主类

2:PhotoRecyclerAdapter 风景图片的适配器代码

3:PhotoDetailActivity  图片详情的活动代码

4:GetPhotoTask 获取网络图片的任务代码 通过调用HTTP接口,从后端服务器获得JSON格式的风景图片信息列表

5:服务端工程的GetPhoto 图片获取接口的服务端代码

四、代码 

GuessLikeActivity

package com.example.chapter14;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener;

import com.example.chapter14.adapter.PhotoRecyclerAdapter;
import com.example.chapter14.bean.PhotoInfo;
import com.example.chapter14.task.resp.GetPhotoResp;
import com.example.chapter14.task.GetPhotoTask;
import com.example.chapter14.widget.SpacesDecoration;
import com.google.gson.Gson;

import java.util.List;

@SuppressLint("DefaultLocale")
public class GuessLikeActivity extends AppCompatActivity implements View.OnClickListener, OnRefreshListener, GetPhotoTask.GetPhotoListener {
    private final static String TAG = "GuessLikeActivity";
    private SwipeRefreshLayout srl_like; // 声明一个下拉刷新布局对象
    private RecyclerView rv_like; // 声明一个循环视图对象
    private PhotoRecyclerAdapter mAdapter; // 声明一个线性适配器对象

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_guess_like);
        TextView tv_title = findViewById(R.id.tv_title);
        tv_title.setText("大美河山");
        findViewById(R.id.iv_back).setOnClickListener(this);
        initRecyclerView(); // 初始化瀑布流布局的循环视图
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.iv_back) {
            finish(); // 关闭当前页面
        }
    }

    // 初始化瀑布流布局的循环视图
    private void initRecyclerView() {
        rv_like = findViewById(R.id.rv_like); // 从布局文件中获取名叫rv_like的循环视图
        // 创建一个垂直方向的瀑布流网格布局管理器
        StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(2, RecyclerView.VERTICAL);
        rv_like.setLayoutManager(manager); // 设置循环视图的布局管理器
        rv_like.addItemDecoration(new SpacesDecoration(1)); // 设置循环视图的空白装饰
        srl_like = findViewById(R.id.srl_like); // 从布局文件中获取名叫srl_like的下拉刷新布局
        srl_like.setOnRefreshListener(this); // 设置下拉刷新布局的下拉刷新监听器
        // 设置下拉刷新布局的进度圆圈颜色
        srl_like.setColorSchemeResources(R.color.red, R.color.orange, R.color.green, R.color.blue);
        srl_like.setRefreshing(true); // 设置状态为正在刷新,此时会弹出进度圆圈
        onRefresh(); // 执行刷新动作
    }

    // 一旦在下拉刷新布局内部往下拉动页面,就触发下拉监听器的onRefresh方法
    @Override
    public void onRefresh() {
        GetPhotoTask task = new GetPhotoTask(); // 创建一个获取照片的异步任务
        task.setGetPhotoListener(this); // 设置照片获取的监听器
        task.execute(); // 把照片获取任务加入到处理队列
    }

    // 在获得照片列表信息后触发
    @Override
    public void onGetPhoto(String resp) {
        srl_like.setRefreshing(false); // 设置状态为正在刷新,此时会关闭进度圆圈
        // 把JSON串转换为对应结构的实体对象
        GetPhotoResp photoResp = new Gson().fromJson(resp, GetPhotoResp.class);
        if (photoResp == null) { // 未获得返回报文,说明HTTP调用失败
            return;
        }
        List<PhotoInfo> photo_list = photoResp.getPhotoList();
        if (photo_list!=null && photo_list.size()>0) {
            Log.d(TAG, "photo_list.size()="+photo_list.size());
            if (mAdapter == null) { // 首次加载前不存在适配器
                // 构建一个照片列表的瀑布流网格适配器
                mAdapter = new PhotoRecyclerAdapter(this, photo_list);
                mAdapter.setOnItemClickListener(mAdapter); // 设置照片列表的点击监听器
                rv_like.setAdapter(mAdapter); // 设置循环视图的瀑布流网格适配器
            } else { // 再次加载时已经存在适配器了
                mAdapter.setPhotoList(photo_list);
                mAdapter.notifyDataSetChanged(); // 通知适配器发生了数据变更
            }
            rv_like.scrollToPosition(0); // 让循环视图滚动到第一项所在的位置
        }
    }
}

PhotoRecyclerAdapter

package com.example.chapter14.adapter;

import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.example.chapter14.PhotoDetailActivity;
import com.example.chapter14.R;
import com.example.chapter14.bean.PhotoInfo;
import com.example.chapter14.util.Utils;
import com.example.chapter14.widget.RecyclerExtras.OnItemClickListener;

import java.util.List;
import java.util.Random;

public class PhotoRecyclerAdapter extends RecyclerView.Adapter<ViewHolder> implements OnItemClickListener {
    private final static String TAG = "PhotoRecyclerAdapter";
    private Context mContext; // 声明一个上下文对象
    private List<PhotoInfo> mPhotoList; // 照片列表

    public PhotoRecyclerAdapter(Context context, List<PhotoInfo> photoList) {
        mContext = context;
        mPhotoList = photoList;
    }

    public void setPhotoList(List<PhotoInfo> photoList) {
        mPhotoList = photoList;
    }

    // 获取列表项的个数
    public int getItemCount() {
        return mPhotoList.size();
    }

    // 创建列表项的视图持有者
    public ViewHolder onCreateViewHolder(ViewGroup vg, int viewType) {
        // 根据布局文件item_photo.xml生成视图对象
        View v = LayoutInflater.from(mContext).inflate(R.layout.item_photo, vg, false);
        return new ItemHolder(v);
    }

    // 绑定列表项的视图持有者
    public void onBindViewHolder(ViewHolder vh, final int position) {
        ItemHolder holder = (ItemHolder) vh;
        PhotoInfo photo = mPhotoList.get(position);
        ViewGroup.LayoutParams params = holder.ll_item.getLayoutParams();
        params.height = 150 + new Random().nextInt(100); // 生成随机高度,从而呈现瀑布流效果
        params.height = Utils.dip2px(mContext, params.height);
        holder.ll_item.setLayoutParams(params);
        holder.tv_title.setText(photo.title);
        // 利用Glide加载网络图片,并在图像视图上显示
        Glide.with(mContext).load(photo.image_url)
                .transition(DrawableTransitionOptions.withCrossFade(1000)) // 设置时长1秒的渐变动画
                .into(holder.iv_pic);
        // 列表项的点击事件需要自己实现
        holder.ll_item.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mOnItemClickListener != null) {
                    mOnItemClickListener.onItemClick(v, position);
                }
            }
        });
    }

    // 获取列表项的类型
    public int getItemViewType(int position) {
        return 0;
    }

    // 获取列表项的编号
    public long getItemId(int position) {
        return position;
    }

    // 定义列表项的视图持有者
    public class ItemHolder extends ViewHolder {
        public LinearLayout ll_item; // 声明列表项的线性布局
        public ImageView iv_pic; // 声明一个照片的图像视图
        public TextView tv_title; // 声明一个标题的文本视图

        public ItemHolder(View v) {
            super(v);
            ll_item = v.findViewById(R.id.ll_item);
            iv_pic = v.findViewById(R.id.iv_pic);
            tv_title = v.findViewById(R.id.tv_title);
        }
    }

    // 声明列表项的点击监听器对象
    private OnItemClickListener mOnItemClickListener;

    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mOnItemClickListener = listener;
    }

    // 处理列表项的点击事件
    public void onItemClick(View view, int position) {
        PhotoInfo photo = mPhotoList.get(position);
        // 以下跳到照片详情页面
        Intent intent = new Intent(mContext, PhotoDetailActivity.class);
        intent.putExtra("title", photo.title);
        intent.putExtra("image_url", photo.image_url);
        mContext.startActivity(intent); // 打开照片详情页面
    }

}

PhotoDetailActivity

package com.example.chapter14;

import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;

public class PhotoDetailActivity extends AppCompatActivity implements View.OnClickListener {
    private final static String TAG = "PhotoDetailActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_photo_detail);
        String title = getIntent().getStringExtra("title");
        String image_url = getIntent().getStringExtra("image_url");
        TextView tv_title = findViewById(R.id.tv_title);
        tv_title.setText(title);
        findViewById(R.id.iv_back).setOnClickListener(this);
        ImageView iv_photo = findViewById(R.id.iv_photo);
        // 构建一个加载网络图片的建造器
        RequestBuilder<Drawable> builder = Glide.with(this).load(image_url)
                .transition(DrawableTransitionOptions.withCrossFade(1000)); // 设置时长1秒的渐变动画
        RequestOptions options = new RequestOptions(); // 创建Glide的请求选项
        options.override(Target.SIZE_ORIGINAL); // 展示原始图片
        options.disallowHardwareConfig(); // 关闭硬件加速,防止过大尺寸的图片加载报错
        // 在图像视图上展示网络图片。apply方法表示启用指定的请求选项
        builder.apply(options).into(iv_photo);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.iv_back) {
            finish(); // 关闭当前页面
        }
    }
}

XML文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/title_tour" />

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/srl_like"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_like"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#eeeeee" />
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>

创作不易 觉得有帮助请 点赞关注收藏~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/6947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ROS】机械人开发二--ROS环境安装

机械人开发二--ROS环境安装一、运行环境二、ROS-melodic安装2.1 设置软件源2.2 设置密钥2.3 安装ROS2.4 环境设置2.5 安装ROS的依赖环境2.6 初始化rosdep三、建立工作空间测试一、运行环境 树莓派4B-4G、VMware15系统都为ubuntu18.04xshell 使用时&#xff0c;通过xshell同时…

10-1.WPF模板

10-1.WPF模板 控件由“算法内容”和“数据内容”决定 算法内容&#xff1a;指控件能展示哪些数据、具有哪些方法、能激发什么事件等&#xff0c;简而言之是控件的功能&#xff0c;一组相关逻辑数据内容&#xff1a;控件所展示的具体数据是什么 在WPF中&#xff0c;模板将数据…

Redis数据类型

1.String(字符串) 在任何一种编程语言中&#xff0c;字符串都是最基础的数据结构&#xff0c;在Redis中String是可以修改的称之为&#xff1a;动态字符串(简称SDS) Redis的内存分配机制&#xff1a; - 当字符串的长度小于1MB时&#xff0c;每次扩容都是加倍现有的空间 - 如果字…

离散数学:图的基本概念

本帖子讨论图的基本概念&#xff0c;这一章&#xff0c;我们将利用有序对和二元关系的概念定义图。图分为了无向图和有向图&#xff0c;他们有共性也有区别&#xff0c;请大家注意体会&#xff0c;用联系和辩证的观点去认识。 1、无向图和有向图 注意无向图和有向图的表示&…

Servlet【 ServletAPI中的会话管理Cookie与Session】

Servlet【 ServletAPI中的会话管理Cookie与Session】&#x1f352;一.回顾Cookie与Session&#x1f34e;1.1 Cookie&#x1f34e;1.2 Session&#x1f34e;1.3Cookie 和 Session 的区别&#x1f352;二.Servlet会话管理操作&#x1f34e;2.1核心方法&#x1f352;三.常见案例实…

【服务器搭建】教程一:没钱买服务器怎么玩 进来看

前言&#xff1a; 最近看到有一些网上的大佬把自己的爱心网页&#xff08;没领到的小伙伴看一下前几篇文章&#xff09;部署到了自己的服务器上&#xff0c;使得可以直接通过链接就实现访问。属实不错&#xff01; 自己内心就产生了这样一个想法&#xff1a;购买一台服务器&a…

Whisper论文阅读笔记

Whisper论文阅读笔记Robust Speech Recognition via Large-Scale Weak Supervision1. 引言2. 方法2.1 数据处理2.2 模型2.3 多任务设置2.4 训练细节3. 实验结果3.1 Zero-shot3.2 多语言语音识别3.3 多语言机器翻译3.4 语种检测3.5 对加性噪声的鲁棒性3.6 长语音转录3.7 人类基线…

基于向量加权平均值的高效优化算法(Matlab代码实现)

&#x1f4dd;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;…

学习 RabbitMQ 这一篇就够了

文章目录一、MQ相关概念二、RabbitMQ相关概念三、安装四、HelloWorld五、工作队列5.1、轮询分发消息5.2、消息应答5.2.1、自动应答5.2.2、手动应答5.2.3、消息自动重新入队5.2.4、消息手动应答代码5.3、持久化5.4、不公平分发&#xff08;能者多劳&#xff09;5.5、预取值5.6、…

读书笔记3|使用Python,networkx对卡勒德胡赛尼三部曲之——《群山回唱》人物关系图谱绘制

读书笔记3|《群山回唱》-卡勒德胡赛尼 踉跄前行中&#xff0c;你总能在他们身上找到丢失的那一部分记忆。 一度看不下去这本书&#xff0c;因为最开始的章节里太痛了&#xff0c;加上我也离开我的孩子&#xff0c;生活已经够苦&#xff0c;我需要一点糖。这次实在太无聊了&…

php-上传图片加水印(文字水印图片水印)

img.php <?php $img 1.jpg; //获取图片信息 $info getimagesize($img); //获取图片类型 $type image_type_to_extension($info[2],false); //在内容中创建一个和图片一模一样的图片 $ext "imagecreatefrom{$type}"; //图片复制到内存中 $image $ext($img);…

基于SpringBoot前后端分离的网吧管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SpringBoot 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目…

PICO《轻世界》体验:随心畅玩,洒脱创作,潜力无限

不少玩家应该还记得&#xff0c;PICO 4发布会上曾宣布将在VR运动健身、VR视频、VR娱乐、VR创造四大方向展开内容布局。而目前&#xff0c;前三个完成了基本部署&#xff0c;在创造方向上则依托于刚刚上线的《轻世界》这款应用。《轻世界》是一款3D内容UGC创作产品&#xff0c;目…

php宝塔部署实战thinkphp考试平台管理系统源码

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 有个朋友发消息跟我说&#xff0c;在网上下载了一套thinkphp考试管理系统的源码&#xff0c;在搭建的时候遇到问题一直部署不起来&#xff0c;让我帮他看看&#xff0c;我看了下代码&#xff0c;里面有些部分代码…

2022年11月华南师范大学自考本科网络工程-本科实践题目

《互联网及其应用&#xff08;03142&#xff09;&#xff08;实践&#xff09;》课程试卷 答卷提交说明&#xff1a;编程代码与输出结果截图&#xff0c;放到一个文件中&#xff0c;文件以“序号 姓名 课程名 ”命名&#xff0c;本试卷有三门课程&#xff0c;请根据不同的课程…

k8s训练营

一、linux命名空间和docker 1.linux的7大ns--------------ipc,net,pid,mnt.uts.user 查看linux的ns lsns查看不同类型的ns [rootmaster ~]# lsns -t netNS TYPE NPROCS PID USER COMMAND 4026531956 net 116 1 root /usr/lib/systemd/systemd --system --deserialize …

公司代码全局参数设置及其意义

在SAP中配置公司时&#xff0c;会配置公司的全局参数&#xff0c;但这些参数具体的意思是什么估计很多同学都搞不懂&#xff0c;我也找了下资料&#xff0c;贴出来供大家参考。 设置参数路径&#xff1a;IMG→财务会计→财务会计全局设置→公司代码的全球参数→输入全局参数 账…

C++Qt开发——Linguist语言家

Qt Linguist 简介 Qt提供了一款优秀的支持Qt C和Qt Quick应用程序的翻译工具。发布者、翻译者和开发者可以使用这款工具来完成他们的任务。 发布者&#xff1a;承担了全面发布应用程序的责任。通常&#xff0c;他们协调开发者和翻译者的工作&#xff0c;可以使用lupdate工具…

激光雷达的厮杀18年:西方“诸神黄昏”,东方“新王隐现”

鼻祖、发明家、神童、梦想家、特种兵和中国双星&#xff0c;激光雷达“诸神混战”&#xff0c;行业疯狂洗牌。 风云激荡中&#xff0c;每个人都在亲身见证历史。 2004年&#xff0c;美国发起DARPA挑战赛&#xff0c;无人车上路&#xff0c;汽车上首次出现激光雷达。 2010年之…

原型工具墨刀的使用

刚开始接触原型工具是大学时候了&#xff0c;大学参加大创的时候第一次接触并使用原型工具做了小程序项目原型。那时候是下载的客户端。 最近&#xff0c;又开始思考在用户沟通过程中为方便沟通&#xff0c;可以先自己用原型工具简单的设计一下先。 首先&#xff1a;网页版好用…