【JavaEE】表白墙终章-飞流直下的“甜言蜜语”-瀑布流式布局
1. 效果前后对比
改进之前:
丑
丑
用户之间无差别
消息先来先到
改进后:
美
美
添加导航栏左上角的身份显示(头像 + 用户名)
采用瀑布流的方式去排列消息
消息以卡片的形式显示(头像 + 用户名 + 日期 + 甜言蜜语)
先来后到
最新发布的排在最前
2. 瀑布流式布局原理思想
瀑布流布局的前提就是,待排序的div元素都是等宽不限高的~
流程:
获取一个div元素盒子的宽
获取底板(父元素)的宽
计算底板最多排列的列数:n
将前四个div元素排在首行
从已排列的每一列中找到最矮的一列,将接下来的元素安插上去(通过绝对定位,即通过提供的坐标,安插到底板的固定位置)
重复5操作,直到全部排序好
动图演示:
越界了怎么办?
加滚动条就行了~
细节:
滚动条不能加在底板上,应该加在底板的至少上一级元素
原因:
刚才在计算列数的时候,是没有减掉滚动条的宽度的,也就是说列数可能是多了1列,导致出现了“水平滚动条”,也就是说“竖直滚动条”挤压了原有元素,会导致一些排版重叠问题!
3. 约定前后端接口
之前的“甜言蜜语”只有简单的正文部分,而现在,它需要有更多的属性:
再原有基础上,应该增加三个属性
image,头像(与用户名绑定)
date,日期(可按时间逆排序)
username,用户名(确认用户信息)
所以,我们需要数据库的一张新的表:
因此,我们需要更改后端代码关于json构造的部分!
4. 后端代码
查询的表
message => vindicate
注意修改!
4.1 修改Love类的定义
class Love { public String from; public String to; public String love; public String username; public String image; public String date; public Love() { //没有这个一定不行!!! //因为后续json构建Love对象需要用到无参的构造方法 } public Love(String from, String to, String love, String username, String image, Timestamp date) { this.from = from; this.to = to; this.love = love; this.username = username; this.image = image; String strn = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); this.date = strn; } }
注意:
jdbc操作数据库,既可以将满足格式的 String类或者 Timestamp类载入数据库datetime类型字段中
jdbc操作数据库,将 datetime类型的字段提取出来的时候,只能提取为 Date或者 Timestamp类(本文选择后者)
这样子,我们在表白墙里看到的就是日期格式了~
当然,日期还有格式还有很多种,这个类还能满足“yyyy/MM/dd HH-mm-ss”、“HH/mm/ss yyyy.MM.dd”等等等…
但是这里,由于后续我将数据载入数据库是用的严格符合格式的字符串的方式,所以选择“yyyy-MM-dd HH:mm:ss”的格式~
还有更多的类表达日期的格式,感兴趣的可以去了解了解,但是我建议,用的时候查就行了
4.2 修改doPost方法
客户端发来的post请求,将客户端json格式的body转化为Love类,载入数据库
当然,客户端那里发过来的post正文是不包含,image、username和date的,所以构造的Love类对象的这三个属性的值为null~
@WebServlet("/love") public class ShowLove extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Love love = objectMapper.readValue(req.getInputStream(), Love.class); HttpSession session = req.getSession(); Timestamp time = new Timestamp(System.currentTimeMillis()); String strn = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time); love.date = strn; love.username = (String)session.getAttribute("username"); love.image = Save.getImage(love.username); save(love); //默认返回的就是200的空报文 } }
然后就是调用save方法,将love对象载入数据库
4.3 修改save方法
private void save(Love love){ DataSource dataSource = new MysqlDataSource(); ((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/Loves?characterEncoding=utf8&useSSL=false"); ((MysqlDataSource)dataSource).setUser("root"); ((MysqlDataSource)dataSource).setPassword("mmsszsd666");//这是俺的微信号,欢迎添加,相互学习! try { Connection connection = dataSource.getConnection(); String sql = "insert into vindicate values(?, ?, ?, ?, ?, ?);"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, love.from); preparedStatement.setString(2, love.to); preparedStatement.setString(3, love.love); preparedStatement.setString(4, love.image); preparedStatement.setString(5, love.date); preparedStatement.setString(6, love.username); preparedStatement.executeUpdate(); preparedStatement.close(); connection.close(); } catch (SQLException e) { e.printStackTrace(); } }
4.4 修改doGet方法
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //如果有输入操作互动的操作,在这里refresh是不合理的,因为写着写着就刷新了,体验不好 // 所以在这里设置refresh没用! //转换为json字符串! List<Love> list = load(); String result = objectMapper.writeValueAsString(list); resp.setContentType("application/json; charset=utf8"); resp.getWriter().write(result); }
这段方法没有更改,主要是load方法更改了
4.5 修改load方法
private List<Love> load(){ DataSource dataSource = new MysqlDataSource(); ((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/Loves?characterEncoding=utf8&useSSL=false"); ((MysqlDataSource)dataSource).setUser("root"); ((MysqlDataSource)dataSource).setPassword("mmsszsd666");//这是俺的微信号,欢迎添加,相互学习! List<Love> list = new ArrayList<>(); try { Connection connection = dataSource.getConnection(); String sql = "select * from vindicate order by date desc;"; PreparedStatement preparedStatement = connection.prepareStatement(sql); //这里的Set并不是,对象为Love的Set集合,而是一个迭代器! ResultSet set = preparedStatement.executeQuery(); //迭代他(是next方法而不是hasnext) while(set.next()) { String from = set.getString("from"); String to = set.getString("to"); String love = set.getString("love"); String username = set.getString("username"); if(username.length() > 9) { username = username.substring(0, 9) + "..."; } String image = set.getString("image"); Timestamp date = set.getTimestamp("date"); list.add(new Love(from, to, love, username, image, date)); } set.close(); preparedStatement.close(); connection.close(); } catch (SQLException e) { e.printStackTrace(); } return list; }
接下来就是本文重点的前端的瀑布流布局实现了
5. 前端瀑布流实现
5.1 规定“甜言蜜语”的层级结构
pic的div块,就是我们的后端数据载入的对象了~
T为信息头,头像显示、用户名显示以及日期显示
B为正文,“甜言蜜语”显示
T的左侧显示头像,右侧显示用户名和日期
5.2 css修饰
在pursue.css里增加
5.2.1 底板div:a
#a { height: calc(100% - 66.67px); width: 100%; /* overflow: auto; */ position: relative; }
将滚动条注释掉,交给其上一级:article
设置属性position为:relative,相对定位 => 与绝对定位对应,如果没有这个属性,我们提供的坐标,其是按照整个页面来定位的
而设置这个属性后,是以此div来定位的(坐标系平移,原点改变)
5.2.2 div:box
这个盒子代表了一条信息的占位空间,主要作用是设立内边距,与其他信息块分离
这个div极为主要
.box { /* 用padding,而不是margin,因为我们计算的时候,我们不希望算上外边距(盒子大小,js获得高度的时候还得加上margin,复杂多了) */ padding: 15px 0 0 15px; height: auto; /* 浮动属性 */ float: left; }
只设置上,与左的内边距为15px
padding:top right bottom left(顺时针)
设置浮动属性:float
子元素div是块级元素,独占一行,设置这个属性后,取消块级性质,并且左排列,越界换行,但是这并不能起到瀑布流的效果
如果接下来的元素可以安插到其中一列,且总长度超过其原最长高,则会补充到对应列
若1不满足,则重起一行
5.2.3 div:pic
.pic { background-color: rgb(255, 255, 255); width: 150px; height: auto; /* 边距 */ padding: 10px; border: 1px solid #ccc; border-radius: 5px; /* 阴影 */ box-shadow: 0 0 5px rgb(0, 0, 0); }
底色为白
宽度固定为150px
高不限制,根据子元素决定
设置内边距为10px,不会导致子元素紧贴边界
设置灰色边框和黑色阴影~
5.2.4 div:T
.pic .T { width: 150px; height: 30px; display: flex; justify-content: space-between; }
消息头的width撑破了限制无所谓~
高度固定为30px => 头像为30px × 30px
设置为弹性布局,并使子元素space-between(根据实际留出间隙)
并且T的左和右是不换行的~
5.2.5 div:Tleft
.Tleft { width: 30px; height: 30px; background-image: url(https://img1.baidu.com/it/u=4205447136,2730860147&fm=253&fmt=auto&app=138&f=JPEG?w=300&h=300); border: 1px solid rgb(0, 0, 0); border-radius: 15px; background-repeat: no-repeat; background-position: center center; background-size: cover; }
设置半径为15px的圆形
图片为默认头像
黑色边框
设置属性是的图片充满圆
5.2.6 div:Tright
.Tright { width: 110px; height: 30px; } .Tright h4 { line-height: 15px; font-size: 15px; color: rgb(128, 128, 128); }
宽度设置为110px,不会导致文字和图片太紧凑
高度设置为30px~
h4标签设置为15px(因为有两个),刚好与div:T等高(字为灰色)
日期可能会超出限制,不过无所谓,在正文的一开始加个换行错开就行了~
5.2.7 div:B
.B { width: 150px; height: auto; word-wrap: break-word; padding-right: 10px; padding-top: 5px; }
宽度为150px
高度由正文决定
根据单词超出限制换行
没有这个设置会导致B的高度始终为一个文字的高度
微调文字布局
5.3 JS实现瀑布流式布局
5.3.1 修改getLoves函数
由于前后端交互接口的改变,这里也需要做一些调整~
function getLoves() { jQuery.ajax({ type: "GET", url: "love", success: function (body) { var boxArray = []; //body就是数组 for (var word of body) { var result = "<br/><h2>" + word.from + "想对" + word.to + "说“" + word.love + "”</h2>"; var aB = jQuery("<div></div>"); aB.attr("class", "B"); aB.append(result); var aTleft = jQuery("<div></div>"); aTleft.attr("class", "Tleft"); console.log(word.image); aTleft.css("background-image", "url(" + word.image + ")"); var aTright = jQuery("<div></div>"); aTright.attr("class", "Tright"); aTright.append( "<h4>" + word.username + ":</h4> <h4>" + word.date + "</h4>" ); var aT = jQuery("<div></div>"); aT.attr("class", "T"); aT.append(aTleft); aT.append(aTright); var aPic = jQuery("<div></div>"); aPic.attr("class", "pic"); aPic.append(aT); aPic.append(aB); var aBox = jQuery("<div></div>"); aBox.attr("class", "box"); aPic.appendTo(aBox); aBox.appendTo(jQuery("#a")); boxArray.push(aBox); } waterFall(boxArray); }, }); }
构造box的时候要细心哦,要契合我们的层级结构!
5.3.2 waterFall函数-瀑布流排列
function waterFall(boxes) { // 将a下的全部box取出来 var oParent = jQuery("#a"); // 计算显示的列数(页面的宽/box的宽) var oBoxWeight = boxes[0].outerWidth(); // console.log(oBoxWeight); var cols = Math.floor(jQuery("#a").width() / oBoxWeight); // 设置a的宽度 oParent.css("width", cols * oBoxWeight + "px"); oParent.css("margin", "0 auto"); // 存放第该列(位置正确摆放的多个div高度)的高度的数组 // 第一次,则是第一行每个div的高度 var hArr = []; for (var i = 0; i < boxes.length; i++) { if (i < cols) { hArr.push(boxes[i].outerHeight()); } else { var minH = Math.min.apply(null, hArr); //top var index = getMinhIndex(hArr, minH); boxes[i].attr( "style", "position: absolute; top: " + minH + "px; left: " + oBoxWeight * index + "px;" ); //处理盒子重叠 hArr[index] += boxes[i].outerHeight(); } } // left -> 左边三个div的宽,top-> 最小高 } function getMinhIndex(arr, val) { for (var i = 0; i < arr.length; i++) { if (arr[i] == val) { return i; } } }
瀑布流布局完成!
5.4 定时刷新
我们可以通过setInterval函数,设置一个函数多久定时调用一次
function refresh() { jQuery("#a").empty(); getLoves(); } setInterval(refresh, 5000);//5000ms
现在我们自动刷新的时机为:
点击发送
定时刷新
6. 测试
手机端:
电脑端: