【Android原生开发】艺术圈APP

项目地址

项目地址github
一个是NodeJS写的服务器(本地),一个是Android端APP

项目背景

艺术来源于生活。以艺术与文化为主体,开发一款APP,主要实现以下五个模块。分别为博物馆模块、时事新闻模块、艺术品模块、用户个人信息模块、用户艺术交流平。整个项目开发主要分为需求分析、Android端艺术圈APP开发、测试系统这四个过程。在这项目开发过程中,首先我会分析用户需求,设计原型并绘制UML图来明确项目中包含的功能块。接着,我会利用图数据库为用户推荐艺术品、博物馆和艺术家。同时,根据用户的浏览艺术品或是参观博物馆等记录对查询语句进行调整和更新。第二,区别于科普类型的应用,用户通过使用APP应用能促使用户自身参与到现实生活中艺术展览的公共场合,优化博物馆参观体验。其三,该应用为用户提供交流分享平台和艺术创作平台,促使人们更多的参与到艺术文化交流的社区之中。从而,让更多热爱艺术品、喜欢参观博物馆或是对艺术有兴趣的人参与其中 。

系统的设计与实现

登录功能

登录界面主要处理登录操作,当用户登录成功之后会返回“登录成功”的消息。同时获取后台传来的用户账号名称、用户的头像。

用户的登录过程

初次运行App,首先进入加载页面,2秒后会进入到导航页。导航页的内容设置在GuideActivity.java中完成 。主要控件为ViewPager,数据为三张图片,左滑翻动页面至最后一页,开始的按钮会显示在手机屏幕的下方。点击“开始”按钮将进入到登录界面。在登录界面要求用户输入用户的账号名称、用户的账号密码。具体业务逻辑在LoginActivity.java中完成。
请添加图片描述

四、密码明文加密及记住密码功能

在登录界面还设置了一个“记住密码”的按钮,初始登录时,会默认选择。记住密码的功能为当用户登录成功之后会记住用户的账号和密码,以便在下一次登录时无需用户再一次输入账号和密码。该功能由SharedPreferences这个轻量级的存储类完成,当用户登录成功之后会判断“记住密码”按钮是否选上。当选上时,则SharedPreferences里面的Editor来存储密码及账号,反之则清除之前存储的数据。而当用户再一次登录App时,则先检测“记住密码”按钮是否被选上,若被选上则,调用getString方法获取对应字段的数据,并提前在要填写账号和密码的位置设置内容。
密码明文传输是及其危险的,那么就需要对密码进行加密。这里用到的是md5加密,但前端并未加盐。在HTTP协议中,传输的是非加密的明文字符串,如果密码是123,那么在网络上传输就是123,只要通过抓包软件进行抓包就可以轻松知道密码是多少; 因此为了不让人看到明文密码,需要加密传输,比如用MD5/SHA-1之类的算法。MD5(123456)之后变成一个32个字符的字符串,此时难以知道原始密码;但是因为MD5算法比较简单一些,网络上有所谓的彩虹表,其实就是保存原始密码及原始密码通过MD5加密之后的结果的2列数据的文件[14],而在这个表上就有一些非常常用的密码的加密结果,此时可以根据你的加密结果来查表,看看是否在表上存在相应的字符串,如果存在,那么可能的密码就是表上的原始密码;因此为了更安全的传输,可以考虑在前端也加盐,MD5(123+固定字符串),只要网络上的黑客不知道前端的盐,那么就算密码为123,他也在表中查不到,此时会更安全;同样在后端保存密码时,一般也不会明文保存,假设数据库泄露了[15],或者被公司里面的开发人员拿到了用户表,那么你的密码直接暴露出来。 在build.gradle中引入依赖包’com.google.guava:guava:24.0-android’,就可以使用md5加密技术,此处我不使用加盐技术。

后端接口定义

登录界面的后端接口为“/login”,后端需要接受前端传来的post请求,并查询数据库中的user表的用户记录。若密码是一致的,则返回用户的头像、用户的账号名称。反之,则返回“登录密码或用户账号不存在”。
在node.js的express框架下,处理post请求需要引入body-parser模块,用于解析post请求body的json数据。在Controllers文件下新建处理用户登录post请求的控制模块,名为userController.js。

login = (req,res) =>{
  const body = req.body;
  const sql = 'select id,userLogo,userName,password from artcircle.user where id = ?';
  const sqlArr = [body.userId];
  const callBack = (err, data) => {
    if (err) {
      console.log('链接出错'+err.toString())
    } else {
      if(body.userPass===data[0].password){
        res.send({
          'code': 0,
          'msg':"登录成功",
          'userLogo':data[0].userLogo,
          'userName':data[0].userName
        })
      }else{
        res.send({
          'code': 1,
          'msg':"用户不存在或密码错误"
        })
      }
    }
  };
  dbConfig.sqlConnect(sql,sqlArr,callBack);
};

用户个性化推荐功能

目前数据库技术有三种数据类型,分别是层次模型、图数据库模型和关系模型。图形数据库将数据可视化,加强了数据之间的关系。同时具有强大的图形遍历功能,也能支持复杂的查询,从而能够极大提高查询的效率。由此,我将选用Neo4j图形数据库构建艺术家、博物馆、艺术品这三者的关系网络。当用户收藏新的内容(博物馆、艺术品、艺术家)时,从图数据库中查询结果自然也会发生变化。该部分用到的接口定义在GraphController模块中,该模块主要处理图数据库的查询。同时还涉及到CollectionController模块中的addCollection接口,该接口用于添加用户个人收藏。

图数据库连接与启动图数据库

在dbconfig.js文件中,通过使用MYSQL连接池的方式连接到本地的MySQL数据库。在dbconfigForNeo4j.js中,我用Node Js连接到我本地的Neo4j数据库。最后在终端输入“node app.js”启动创建express工程,从而分别连接上本地的MySQL数据库和Neo4j图数据库。

const mysql = require('mysql');
module.exports = {
  //数据库配置
  config: {
    host: '127.0.0.1',
    port: '3306',
    user: 'ArtCircle',
    password: 'ArtCircle',
    database: 'ArtCircle',
  },
  //连接数据库,使用mysql的连接池方式
  //连接池对象
  sqlConnect : function (sql, sqlArr, callBack) {
    var pool = mysql.createPool(this.config);
    pool.getConnection((err,conn)=>{
      console.log("sql: "+sql+" - sqlArr: "+sqlArr);
      if(err){
        console.log('连接失败');
        console.log(err.toString());
        return;
      }
      //事件驱动回调
      conn.query(sql,sqlArr,callBack);
      //释放连接
      conn.release();
    })
  }
};

const neo4j = require("neo4j-driver");
const driver = neo4j.driver('bolt://localhost', neo4j.auth.basic('ArtCircle','123456'));
const session = driver.session();
module.exports = {
  session
};

图数据库查询语句与接口定义

实现这部分的功能涉及的接口说明表如表5.1所示。对于搭建图数据库,构建后的数据根据Neo4j图数据库组织模式进行存储,使用Neo4j图数据库内置的Cypher数据库语言进行查询、删除、更新等操作。

方法功能
addCollection添加该项至用户个人收藏
getMyCollection根据userId和type查询个人收藏
getMyCollection根据userId和type查询个人收藏
getRecommendForArtworkFromArtist根据艺术家推荐艺术品(图数据库)
getRecommendForArtworkFromMuseum根据博物馆推荐艺术品(图数据库)
getRecommendForArtistFromArtwork根据艺术品推荐艺术家(图数据库)
getRecommendForMuseumFromArtwork根据艺术品推荐博物馆(图数据库)
Cypher语句操作类型
create (province:PROVINCE {name:’省份名称’}) – [:in] -> (country:COUNTRY {name:’国家名称’})创建某一个省份实体对象和一个国家实体对象,并建立联系
create (museum:MUSEUM {name:'博物馆’名称}) - [:locate] -> (province:PROVINCE {name:’省份名称’})创建某一个博物馆实体与一个省份实体对象,并建立联系
create (:ARTWORK {name:‘艺术品名称’}) -[:house] -> (:MUSEUM {name:‘博物馆名称’})创建某一个艺术品实体与一个博物馆实体对象,并建立联系
create (:ARTIST {name:‘艺术家名称’}) -[:create] -> (:ARTWORK {name:‘艺术品名称’})创建某一个艺术家实体对象与一个艺术品实体对象,并建立联系
match p = shortestpath((:ARTIST{name:‘艺术家名称’})-[*…${k}]-(artwork:ARTWORK)) return artwork limit 100根据艺术家名称查询某一个艺术家实体和艺术品实体对象之间距离小于等于k的路径
match p = shortestpath((:MUSEUM{name:‘博物馆名称’})-[*…${k}]-(artwork:ARTWORK)) return artwork limit 100根据博物馆名称查询某一个博物馆实体和艺术品实体对象之间距离小于等于k的路径
match p = shortestpath((:ARTWORK{name:‘艺术品名称’})-[*…${k}]-(artist:ARTIST)) return artist limit 100根据艺术品名称查询某一个艺术品实体和某一个艺术家实体对象之间距离小于等于k的路径
match(museum:MUSEUM)-[:house]-(artwork:ARTWORK) where museum.name= ‘博物馆名称’ return artwork根据名称查询某一个博物馆下的所有艺术品

图数据库的连接与数据返回

目前前端界面已经获取用户最初喜欢的某一些艺术品、某几位艺术家以及某几个博物馆之后,同时也建立了前文设计的图数据库,那么在Node.js中执行Cypher语句,我能得到该用户可能会喜欢的艺术品、艺术家以及博物馆。如:当用户对“顾恺之”这位艺术家较为感兴趣,或者用户曾经浏览过这位艺术家。同时用户还对浙江省博物馆比较感兴趣,那么我将根据这两点在图数据中查询并推荐给用户可能会喜欢的艺术品。用户点击主页中的艺术品推荐按钮时调用RecommendController.js中的控制模块分别为getRecommendForArtworkFromArtist模块和getRecommendForArtworkFromMuseum模块对图数据库查询。查询结果为用户可能会喜欢的艺术品。在neo4j中分别执行两个模块中的查询语句则会显示出如图所示的结果。左侧为与“浙江省博物馆”相邻的艺术品,右侧为与“顾恺之”相邻的艺术品。

getRecommendForArtworkFromArtist = (req,res) =>{
  let {artist,k} = req.query;
  const sql = `match p = shortestpath((:ARTIST{name:'${artist}'})-[*..${k}]-(artwork:ARTWORK)) return artwork limit 100`;
  session.run(sql).then(function (result) {
    res.send({
      'list':result.records
    })
  }).catch(function (err) {
    console.log(err);
  })
};
getRecommendForArtworkFromMuseum = (req, res) =>{
  let {museum, k} = req.query;
  const sql = `match p = shortestpath((:MUSEUM{name:'${museum}'})-[*..${k}]-(artwork:ARTWORK)) return artwork limit 100`;
  // const sql = `match p = shortestpath((:MUSEUM{name:'故宫博物馆'})-[*..1]-(artwork:ARTWORK)) return artwork limit 100`;
  session.run(sql).then(function (result) {
    res.send({
      'list':result.records
    })
  }).catch(function (err) {
    console.log(err);
  })
};

在这里插入图片描述

前端界面布局及数据处理

这个Activity主要显示后端根据用户浏览记录、爱好来推荐的艺术品。当多个结果返回是,可能会存在艺术品的重复推荐,那么,为了使得数据不重复,则存储对象时采用set存储数据,并重写hashCode()方法和equal()方法。
当用户初次登录时,会进入到LikeActivity.java中,让用户选择自己感兴趣的艺术品、艺术家、博物馆。当用户选择完后,进入到下一步时,后台会记录用户选择的内容,并在collection表中添加用户个人的收藏记录。表示为用户已经收藏了这些内容。由此,用户获取推荐内容,即是参考用户个人已收藏的内容。
同理,艺术家推荐、博物馆推荐功能实现原理也和艺术品推荐的实现过程相似,在这里不再赘述。
在这里插入图片描述

时事新闻界面推送功能

时事新闻界面之初步配置NewsActivity.java

该界面包含艺术品新闻的推送和博物馆新闻推送。在Android studio中,我创建了名为ArtCircle的工程项目。 在包下创建了用于完成前端时事新闻界面设计的NewsActivity.js活动。通过封装一个HttpGetUtil.java类来启动一个线程,同时带上MYURL+NEWS参数(即访问的网址),我将可以轻松访问到用NodeJs编写的后端接口提供给的数据。针对在获取新闻数据时,特别是新闻的封面图片,可能会导致加载过慢。于是我引入了他人在github上发布的工具类glide,需要在android目录下的build.gradle文件中添加依赖’com.github.bumptech.glide:glide:4.0.0’,该类可以在ImageView中加载网址类型的图片,还实现缓存技术。

HttpGetUtils httpGetUtils = new HttpGetUtils();
        httpGetUtils.setOnSuccessListener(new HttpGetUtils.OnSuccessListener() {
            @Override
            public void onSuccessGet() {
                Log.i("TEST", "返回结果为" + httpGetUtils.getResponseData());
                try {
                    JSONObject data = new JSONObject(httpGetUtils.getResponseData());
                    JSONArray list = data.getJSONArray("list");
                    for (int i = 0; i < list.length(); i++) {
                        info2 temp = new info2();
                        temp.setId(list.getJSONObject(i).getString("id"));
                        temp.setTime(list.getJSONObject(i).getString("time"));
                        temp.setType(list.getJSONObject(i).getInt("type"));
                        temp.setUrl(list.getJSONObject(i).getString("url"));
                        temp.setTitle(list.getJSONObject(i).getString("title"));
                        temp.setCover(list.getJSONObject(i).getString("cover"));
                        temp.setContent(list.getJSONObject(i).getString("content"));
                        temp.setKeyword(list.getJSONObject(i).getString("keyword"));
                        temp.setEditor(list.getJSONObject(i).getString("editor"));
                        temp.setSource(list.getJSONObject(i).getString("source"));
                        newsContentList.add(temp);
                    }
                    //加载布局-需在获取数据之后
                    mRecyclerView = view.findViewById(R.id.newsContentList);
                    RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
                    mRecyclerView.setLayoutManager(layoutManager);
                    adapter = new newscontentAdapter(newsContentList, getActivity());
                    mAdapter = new RecyclerViewMaterialAdapter(adapter);
                    mRecyclerView.setAdapter(mAdapter);
                    MaterialViewPagerHelper.registerRecyclerView(getActivity(), mRecyclerView, null);
                    adapter.notifyDataSetChanged();
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
        httpGetUtils.execute(MYURL + NEWS, "type", "3");

时事新闻界面的设计

考虑到会有不同种类的新闻,我引入了他人在github上发布的工具类MaterialViewPager(添加依赖’com.github.florent37:materialviewpager:1.1.3@aar’),设置适配器和获取数据之后,时事新闻界面如图所示。其中时事新闻分为四类,根据type字段的不同,可以获取不同种类的新闻。用户可以在当前界面中阅读博物馆类新闻、艺术品类新闻、艺术家类新闻、拍卖类新闻。用户既可以点击小标签来切换成不同类型的新闻,也可以向左划屏幕或右划屏幕来切换。
在这里插入图片描述
当用户点击不同新闻时,会进入到不同的新闻详情页。新闻详情页主要包含新闻的标题、新闻的封面、新闻发布的时间、新闻内容简介、新闻来源、新闻原文链接、新闻的编辑者、新闻的关键字。我在网址上设置监听事件,当点击后跳转至UrlActivity.java,并在该布局中添加webView控件,设置相关属性后,可以使得出现的网页自适应屏幕,用户也可以进行放大网页等操作。由此用户点击原文网址之后就可以浏览新闻的原文网址。
在这里插入图片描述

创建二维码功能和扫描二维码功能

初始化CreateQRActivity.java

通过之前封装的HttpGetUtil.java类来获取所有艺术品的基本信息,包括艺术品的名称、艺术品官网的网址等等。当艺术品过多时,用户可能会用到搜索的功能,该搜索的功能实现包括三个过程,分别是在适配器中实现FilterListener的接口、设置每一项的监听事件、点击之后调用CodeUtils.createQRCode来构造艺术品二维码。最终实现的效果如图所示。
在这里插入图片描述

初始化ScanActivity.java

当为每一个艺术品都创建了不同的二维码之后,要在App中初始化扫描二维码的活动。首先,需要动态获取摄像头的权限申请,当App没有访问手机摄像头的权限时,使用requestPermissions方法来动态获取权限。当用户允许App使用“拍摄照片和录制视频”,会打开手机的摄像头。扫描到由CreateQRActivity.java生成的二维码之后,会打开该件艺术品的官方网站。识别二维码的功能需要在build.gradle中加入依赖’com.king.zxing:zxing-lite:1.1.7-androidx’,那么就可以调用CaptureActivity.java来启动手机扫描二维码的功能。通过重写onActivityResult方法来获取扫描到的结果。整个流程如图所示。
在这里插入图片描述

画板功能

初始化画布类DrawingBoard.java

该类继承View,共有两种模式,分别为画笔模式、橡皮擦模式。橡皮擦模式可以看做是画布色的画笔在画布上绘制线条。绘制图案需要Canvas,Canvas绘图有三个要素:画布、绘图坐标系以及画笔。手指触碰屏幕时有三种状态,分别为ACTION_DOWN、ACTION_MOVE、ACTION_UP。针对这三种情况要利用Path、Canvas分别处理。最后,还有两个功能,分别为反撤销操作和撤销操作,对于反撤销操作,需要定义一个列表记录绘制的路径。画布的侧边还有画笔大小调节,以及画布另一边有颜色选择。

public boolean onTouchEvent(MotionEvent event) {

        float x =event.getX();
        float y =event.getY();
        switch (event.getAction())

        {
            case MotionEvent.ACTION_DOWN:

                mLastX = x;
                mLastY = y;
                mPath.moveTo(mLastX,mLastY);
                break;
            case MotionEvent.ACTION_MOVE:
                //绘制画出的图形
                mPath.quadTo(mLastX,mLastY,(mLastX+x)/2,(mLastY+y)/2);
                mBufferCanvas.drawPath(mPath,mPaint);
                invalidate();
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                //保存路径
                savePath();
                mPath.reset();
                break;

        }

        return true;
    }


public void clean(){

        savePaths.clear();
        currPaths.clear();
        //将位图变为透明的
        mBufferBitmap.eraseColor(Color.TRANSPARENT);
        invalidate();
    }
    /**
     * 下一步 反撤销
     * */
    public void nextStep() {
        if (currPaths.size() > 0) {
            currPaths.remove(currPaths.size() - 1);
            reDrawBitmap();
        }
    }

    public int getmPaintSize() {
        return mPaintSize;
    }

    public void setmPaintSize(int mPaintSize) {
        this.mPaintSize = mPaintSize;
        mPaint.setStrokeWidth(mPaintSize);
    }

    /**
     * 上一步 撤销
     * */
    public void lastStep(){
        if (currPaths != savePaths)
        {
            if (savePaths.size()>currPaths.size()){
                currPaths.add(savePaths.get(savePaths.size()-1));
                reDrawBitmap();
            }
        }
    }
    //重绘位图
    private void reDrawBitmap(){
        mBufferBitmap.eraseColor(Color.TRANSPARENT);
        for (int i=0;i<currPaths.size();i++){
            DrawPathList path = currPaths.get(i);
            mBufferCanvas.drawPath(path.getPath(),path.getPaint());
        }
        invalidate();
    }

在这里插入图片描述

用户个人信息模块

后端提供数据的接口

接口分别是getMyAchievement、getMyHistory、getMyShare和getCollection,分别提供用户的成就、用户的历史记录、用户个人动态、用户收藏,每个接口的具体功能如表所示。

方法功能
getMyAchievement根据用户id查询成就
getMyHistory根据用户id查询历史记录
getMyShare根据用户id查询用户动态
getCollection根据用户id和类型查询收藏

初始化mineActivity.java

在这里采用前文用到的MaterialViewPager,同时我设计了五个不同的类,他们都继承Fragment,分别在类中写一个静态接口。在mineActivity中设置MaterialViewPagerListener监听方法,从而可以根据当前序号对应显示内容,关键代码如附录5.8所示。这五个类分别是FragmentForOne.java、FragmentForTwo.java、FragmentForThree.java、FragmentForFour.java、FragmentForFive.java,分别用于显示用户基本信息、用户成就、用户动态、用户收藏、浏览历史。

用户个人信息界面的五块内容

通过已经封装好的HttpGetUtils类,可以获取后台传来的数据。在布局中设置一个RecyclerView,并根据显示的内容对应设置适配器。当获取后台数据的线程得到数据之后就可以调用适配器的notifyDataSetChanged方法来更新界面,最终实现的效果如图所示。在这里插入图片描述

首页中的新增个人动态功能

用户可以点击主页中“新增”按钮,输入动态的标题、内容以及上传一张封面图片,即可创建一个个人动态。后端对应接口为CreateShare模块中的createShare和upLoadImage。其中,上传图片前端需要构造一个HttpPostFileUtil类,该类继承AsyncTask,其实就是开启一个线程。参数为File,采用formData的形式上传文件。接着,需要用户获取图片,那么就还有跳转到手机的Imag目录下,并监听用户选择了哪张图片。当用点击其中某一张图片之后,将该图片载入内存,并获取该图片暂时性的存储地址。最后,将该图片以File类型的参数传给上述创建的线程类,并开启该线程。在这里插入图片描述

public void uploadFile() {
        File file = new File(imgPath);
        HttpPostFileUtil thread = new HttpPostFileUtil();
        thread.setOnSuccessListener(new HttpPostFileUtil.OnSuccessListener() {
            @Override
            public void onSuccessPost() {
                Log.i("TEST",thread.getResponseData());
                try {
                    JSONObject result = new JSONObject(thread.getResponseData());
                    cover = result.getString("msg");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.execute(file);
    }

上传图片的后端接受该图片文件时,得到的是二进制流,首先,将该二进制流文件暂存至uploads目录下,该操作需要引入multer模块。接着,引入fs模块用于读取uploads目录下这个图片文件的内容(fs.readFile)。为了避免文件名相同而覆盖之前上传的文件,文件命名则采用时间加随机数字的命名方法。最后一步,调用fs的writeFile方法读取二进制流文件,同时给文件加上文件类型,并存储在静态目录对应的文件夹(/public/images/share/)下。

upLoadImage = (req, res) => {
  fs.readFile(req.file.path, (err, data) => {
    //读取失败,说明没有上传成功
    if (err) {
      return res.send('上传失败')
    }
    let time = Date.now() + parseInt(Math.random() * 10000 + "");
    let extname = req.file.mimetype.split('/');
    let keepname = time + '.' + extname;
    fs.writeFile(path.join(__dirname, '../public/images/share/' + keepname), data, (err) => {
      if (err) {
        return res.send('上传失败')
      }
      res.send({code: 0, msg: "/images/share/" + keepname})
    })
  })
};

博物馆详情界面

博物馆体验模块分为四部分,分别为博物馆简介、博物馆计划、博物馆文物概览、博物馆地图导航。

初始化MuseumDetailActivity.java

在活动中仍然使用MaterialViewPager来做布局空间。用户会点击任意一个博物馆进入博物馆详情界面。从而在博物馆推荐界面中的博物馆设置监听事件,当发生点击事件时,跳转至博物馆详情界面。
一共有四块内容,则新建四个Fragment,分别是Artwork.java、Introduction.java、MapToMuseum.java、Plan.java。 Introduction.java在布局文件中设置了几个TextView,当从接口中获取数据之后就显示在对应的控件上。Plan.java和Artwork.java需要从MySQL数据库和图数据库中获取数据。当从接口中得到数据后就更新界面,方法已在前文介绍过,此处不再赘述。关于MapToMuseum.java中的地图导航,需要开启第三方软件(百度地图),并指定目的地为某一座博物馆的名称,当用跳转至百度地图时,会自动定位到该位置,从而实现导航。
在这里插入图片描述

baiduMap.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Uri uri= Uri.parse("baidumap://map/geocoder?src=andr.baidu.openAPIdemo&address="+MUSEUMNAME);  //打开地图定位
                Intent it = new Intent(Intent.ACTION_VIEW, uri);
                ComponentName cn = it.resolveActivity(getContext().getPackageManager());
                if(cn == null){
                    Toast.makeText(getContext(),"请先安装第三方导航软件",Toast.LENGTH_SHORT).show();
                }else{
                    Log.i("TEST",cn.getPackageName());
                    startActivity(it);
                }
            }
        });

后端接口定义

后端接口定义MuseumController.js中,接口分别是getMuseumIntroduction、getMuseumArtwork、getMuseumPlan,具体返回的结果如表所示。

方法功能
getMuseumIntroduction根据用户museumId查询博物馆基本信息
getMuseumArtwork根据用户museumName查询博物馆下的所有艺术品
getMuseumPlan根据用户museumId查询用户在该博物馆的所有计划
addCollection添加收藏
deleteCollection删除收藏
judgeExist判断是否存在该收藏
getMuseumIntroduction、getMuseumPlan是查询关系数据库,getMuseumArtwork是查询图数据。

博物馆游览计划模块

界面布局

在博物馆详情界面的计划这一界面中,加入一个新增计划的按钮。当用户点击添加按钮之后会弹出一个对话框。这里的对话框采用AlertDialog来新增一个对话框。这个对话框采用自定义的布局,在这个自定义布局中,设置了两个输入框,分别需要用户输入“计划的时间”和“计划的备注”。同时还放了“确定”按钮和“取消”按钮。为了让用户选择日期和时间,在build.gradle中加入依赖语句implementation 'com.google.android.material:material:1.1.0’和implementation ‘com.github.loperSeven:DateTimePicker:0.1.0’。这样可以使用CardDatePickerDialog类来实现“用户选择日期时间”的交互效果。当用户填完数据之后,并点击“确定”按钮,那么向后台发送新纪录的数据。发送的数据包括用户的id、博物馆的名称、博物馆的id、计划的备注、计划的时间。在主页的侧边栏中,点击计划按钮,可以查看到日历表。使用日历表,先在build.gradle中加入依赖语句“implementation ‘com.haibin:calendarview:3.6.8’”。在布局加入CalendarLayout控件,同时在里面加入CalendarView控件和RecyclerView控件。分别用于显示日历和显示计划列表。对于这个控件,要想构造出圆圈形状的进度条样式,需要设置app:month_view、app:week_view这两个重要属性。对于这两个属性需要用到ProgressMonthView和ProgressWeekView,这两个类,本人参考了作者样例的源代码,在这里不再详细描述。对于日历初始化的数据,传入参数的数据结构为map<String,Calendar>,同时还需要计算某一天中已完成计划占当天计划总数的百分比。这就需要对从后台传来的数据先存储至map<String, List>,根据计划状态的属性,计算百分比。在这里插入图片描述

<com.haibin.calendarview.CalendarLayout
        android:id="@+id/calendarLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/color5"
        android:orientation="vertical"
        app:calendar_content_view_id="@+id/recyclerView">
        <com.haibin.calendarview.CalendarView
            android:id="@+id/calendarView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#fff"
            app:calendar_height="52dp"
            app:current_month_lunar_text_color="#CFCFCF"
            app:current_month_text_color="@color/color4"
            app:day_text_size="14sp"
            app:max_year="2020"
            app:min_year="2004"
            app:month_view="com.example.art.museumPlan.plan.ProgressMonthView"
            app:month_view_show_mode="mode_fix"
            app:other_month_lunar_text_color="#e1e1e1"
            app:other_month_text_color="#e1e1e1"
            app:scheme_text=""
            app:scheme_text_color="#333"
            app:scheme_theme_color="#128c4b"
            app:selected_lunar_text_color="#CFCFCF"
            app:selected_text_color="@color/color4"
            app:selected_theme_color="#FFf54a00"
            app:week_background="@color/color1"
            app:week_text_color="#111111"
            app:week_view="com.example.art.museumPlan.plan.ProgressWeekView"
            app:year_view_day_text_color="#333333"
            app:year_view_day_text_size="9sp"
            app:year_view_month_text_color="#ff0000"
            app:year_view_month_text_size="20sp"
            app:year_view_scheme_color="#f17706" />
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/planRecylerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/color5_1"
            android:padding="10dp"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            app:spanCount="1" />
    </com.haibin.calendarview.CalendarLayout>

Map<String, Calendar> map = new HashMap<>();
        for(Map.Entry<String, List<MuseumItem>> entry : data.entrySet()){
            String mapKey = entry.getKey();
            List<MuseumItem> mapValue = entry.getValue();
            int status1 = 0;
            for(int i=0;i<mapValue.size();i++){
                if(mapValue.get(i).getStatus()==1){
                    status1++;
                }
            }
            final String text = "" + Math.round(1.0 * status1 / mapValue.size() * 100);
            map.put(getSchemeCalendar(Integer.parseInt(mapKey.substring(0,4)), Integer.parseInt(mapKey.substring(4,6)), Integer.parseInt(mapKey.substring(6,8)), 0xFF40db25, text).toString(),
                    getSchemeCalendar(Integer.parseInt(mapKey.substring(0,4)), Integer.parseInt(mapKey.substring(4,6)), Integer.parseInt(mapKey.substring(6,8)), 0xFF40db25, text));
        }
        mCalendarView.setSchemeDate(map);
 

后端接口设置

在MuseumController模块中,添加addPlan和modifyPlan两个接口,分别用户添加计划和修改计划。这两个接口采用post请求,都是对表plan进行操作。其中addPlan中执行的SQL语句是‘INSERT INTO artcircle.plan (id, time, museumId, note, status, userId, museumName) VALUES (?, ?, ?, ?, ?, ?, ?);’,而modifyPlan执行的SQL语句是’UPDATE artcircle.plan SET status = ? WHERE (id = ?);'语句。

模糊搜索功能及主页中的轮播图

界面布局

为了使用户操作便捷,减少用户操作步骤,主页的布局用DrawerLayout控件作为最外层,里面嵌套NavigationView、ConstraintLayout、NestedScrollView等控件。
在build.gradle文件中引入’com.github.arimorty:floatingsearchview:2.1.1’。由此,在主页中,顶部的搜索栏则可以直接使用FloatingSearchView定义。搜索栏下方的轮播图由ShufflingAdapter适配器构建出,该适配器继承PagerAdapter。为了能让轮播图每隔一段时间切换一张图片,则创建一个线程Handler,每隔3秒替换当前的显示内容。在轮播图的下方是时事新闻按钮和其余三个用户个性化推荐的按钮,其中时事新闻按钮是时事新闻推送,推送内容包括博物馆新闻、艺术品新闻、艺术家新闻、拍卖新闻。三个用户个性化推荐的按钮分别是艺术品推荐功能的入口、艺术家推荐功能的入口、博物馆推荐功能的入口。再者是三块主题内容,分别是艺术鉴赏的主题、拍卖艺术品的主题、创造艺术的主题。再往下则是一些最新的动态内容,主页还设置了侧边栏,手势右划可以看到登录用户个人的账号和登出用户的账号,以及侧边栏下方的菜单有扫描艺术品的二维码功能、生成艺术品二维码的功能等。
在这里插入图片描述

艺术主题模块

界面布局

首先引入依赖’co.lujun:androidtagview:1.1.7’,这样可以调用TagContainerLayout类来绘制标签类型的图案,其中需要输入参数为List的链队。当点击其中的某一个标签项时,就将当前标签内的文字(如:中国画)作为参数传递给线程,并跳转至新的Activity,当线程获取到数据之后,就可以更新界面。这里的数据来源于图数据库,在界面中可以点击左上角的“心”形状的按钮,表示收藏。在这里插入图片描述

后端接口及返回的数据

后端对应的接口为定义在ThemeController.js模块中的getThemeForTag接口。执行图数据库查询语言获取某一个标签周围的艺术品,查询语句为match(n)-[r:type]-(m:ARTWORK) where n.name=‘${tag}’ return m。返回的数据的方式和之前获取数据的方式类似,在Neo4j desktop中执行该查询语句可以得到如图所示的查询结果。
在这里插入图片描述

用户动态模块

界面布局

在主页中,设置一个RecyclerView用于显示最新的动态,为动态的封面设置监听事件,当点击时跳转至ShareContentActivity.java中。用户可以查看该动态的详细内容,包括作者姓名、动态标题、动态内容、发布时间以及评论。对于该动态,用户还可以进行点赞或者取消点赞、评论的操作。对于点赞按钮,我使用了工具类LikeButton,当然也可以加入依赖语句’com.github.jd-alexander: LikeButt on:0.2.3’,对于评论部分,同样是设置一个RecyclerView显示评论的列表。在这里插入图片描述

后端接口定义

在动态模块,主要使用到的接口如表所示。在某一个用户的动态下,所有用户都可以在底下评论,或是点赞。根据需求分析的结果(见第二章动态模块中的活动图),在该界面的逻辑处理过程如下:当用户点击进入动态详情界面时,除了获取评论列表和动态的基本信息之外,还会向后台调用‘/findFavor’接口来查询对于该用户对于这一篇文章的点赞状态,若未查询到记录,则在favor表中添加一条记录,设置isFavored字段为0,表示未点赞,并返回结果。若查询到结果后,并且返回的isFavored字段为1,则设置“点赞按钮”为选中状态;当用户点击该按钮时,调用‘/modifyFavor’接口来修改对应记录的isFavored的数值。再是用户评论部分,当用户评论时,会调用‘/addComments’接口向comments表添加一条记录。评论和点赞功能完成之后,都会修改share表中对应的动态记录的favor字段和comment字段。比如:添加评论会让comment字段自增1,点赞操作会使favor字段自增1,取消点赞会使favor字段自减1。

方法功能
findFavor获取某一篇动态的点赞状态
ModifyFavor修改点赞状态
addComments添加评论
  • 26
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sheng_er_sheng

打赏是什么?好吃么

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值