Day19 - 读取更多推文

今天来做显示推文和换页读取更多推文的部分。

Layout

Day16时PreviewFragment的Layout只单纯放TextView显示文章内容,今天的内容首先是先把TextView拿掉改成RecyclerView,这部份很单纯就不放程序码了,主要还是在RecyclerView的Item Layout:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:background="@color/transparent"
    android:paddingStart="@dimen/one_grid_unit"
    android:paddingTop="@dimen/half_grid_unit"
    android:paddingEnd="@dimen/one_grid_unit"
    android:paddingBottom="@dimen/half_grid_unit">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/like"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:fontFamily="sans-serif"
            android:textSize="16sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="推" />

        <TextView
            android:id="@+id/id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/one_grid_unit"
            android:fontFamily="sans-serif"
            android:textColor="@color/userid"
            android:textSize="16sp"
            app:layout_constraintStart_toEndOf="@id/like"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="AN511" />

        <TextView
            android:id="@+id/time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:fontFamily="sans-serif"
            android:textColor="@color/text_normal"
            android:textSize="16sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="10/03 12:40" />

        <TextView
            android:id="@+id/content"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:fontFamily="sans-serif"
            android:maxLines="2"
            android:textColor="@color/content"
            android:textSize="16sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="@id/id"
            app:layout_constraintTop_toBottomOf="@id/like"
            tools:text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

预览画面:
https://ithelp.ithome.com.tw/upload/images/20211004/2012460215Ps11hGbq.png

Adapter

接着是RecyclerView的Adapter设计

CommentAdapter

class CommentAdapter : RecyclerView.Adapter<CommentAdapter.ViewHolder>() {
    private val commentList = mutableListOf<Comment>()
    var moreCommentCallback: (() -> Unit)? = null

    public fun setData(newList: List<Comment>) {
        val result = DiffUtil.calculateDiff(CommentDiffUtilCallbackImpl(commentList, newList))
        commentList.clear()
        commentList.addAll(newList)
        result.dispatchUpdatesTo(this)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 
        ViewHolder(
            ItemCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )

    override fun getItemCount(): Int = commentList.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bindView(commentList[position])
        if (position == 0) {
            moreCommentCallback?.invoke()
        }
    }

    inner class ViewHolder(private val binding: ItemCommentBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bindView(comment: Comment) {
            binding.like.text = comment.like
            when (comment.like) {
                "推" -> {
                    binding.like.setTextColor(itemView.context.getColor(R.color.label_push))
                }
                "嘘" -> {
                    binding.like.setTextColor(itemView.context.getColor(R.color.label_boo))
                }
                "→" -> {
                    binding.like.setTextColor(itemView.context.getColor(R.color.label_normal))
                }
            }

            binding.id.text = comment.id
            binding.time.text = comment.time
            binding.content.text = comment.content
        }
    }
}

基本上结合Day17Day18的内容,不算有太特别的地方,值得一提的是呼叫moreCommentCallback的条件,设为if (position == 0),这是因为我的RecyclerView会设为倒序读取,因此最开始显示的会是commentList最後一项并往前读取内容。

PreviewFragment

最後看一下Fragment的内容,其中会分成几个部分:parseComments、RecyclerView设定、moreCommentCallback。

parseComments

基本上是将Day17的内容提取成一个method再稍作修改:

private val adapter = CommentAdapter()
private val commentList = mutableListOf<Comment>()
private var hasMore = true

private fun parseComments(screen: String) {
    val rows = screen.split("\n")
    val commentPattern =
        Pattern.compile("(?<like>[推]|[→]|[嘘])*[ ](?<id>.*)[:][ ](?<content>.*)[ ](?<ip>((.*\\.){3}.*|[ ]))(?<date>../.. ..:..)")
    val currentCommentList = mutableListOf<Comment>()
    rows.forEach {
        val matcher = commentPattern.matcher(it)
        if (matcher.find()) {
            val like = matcher.group("like")!!.trim()
            val id = matcher.group("id")!!.trim()
            val content = matcher.group("content")!!.trim()
            val ip = matcher.group("ip")!!.trim()
            val date = matcher.group("date")!!.trim()

            val comment = Comment(
                like,
                id,
                content,
                ip,
                date
            )
            Log.d(mTag, "comment:$comment")
            if (!commentList.contains(comment)) {
                currentCommentList.add(comment)
            }
        }
    }
    hasMore = currentCommentList.isNotEmpty()
    commentList.addAll(0, currentCommentList)
    adapter.setData(commentList)
}

commentList是用来储存已解析的所有推文,currentCommentList则是目前页面的推文内容,每加入新的currentCommentList时都要加在commentList的最前面,以确保旧的推文内容在List前面的位置。

示意图
https://ithelp.ithome.com.tw/upload/images/20211004/20124602q1eJMsNDmX.png

hasMore是用来判断是否还有推文能更新,判断的依据就是看这次解析出来的currentCommentList是否为空,为空的话代表已无内容可在新增。会这样判断是因为Ptt的内文是可以让使用者任意修改的,用其它Pattern都有不存在的风险,因此使用这轮是否还有解析东西来判断是我目前思考後比较适合的方法。

RecyclerView设定

private val adapter = CommentAdapter()

binding.recyclerView.apply {
    layoutManager = LinearLayoutManager(requireContext()).apply {
        stackFromEnd = true
    }
    setHasFixedSize(true)
    adapter = [email protected]
}

主要就是使用LinearLayoutManagersetStackFromEnd方法来让内容倒序读取

moreCommentCallback

首先要看一下Ptt文章内的相关指令
https://ithelp.ithome.com.tw/upload/images/20211004/20124602OR8EJJdYDF.png
可以看到上翻一页的指令有四种:^B ^H PgUp BS。(^代表的是ctrl按键)
接着根据ASCII Table可以查到^B^H各自的Char数值,这边我是使用^B来做为向上一页的指令。

adapter.moreCommentCallback = {
    if (hasMore) {
        viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
            PttClient.getInstance().send(Char(2).toString())
            delay(100L)
            Log.d(mTag, "current page:\n${PttClient.getInstance().getScreen()}")
            parseComments(PttClient.getInstance().getScreen())
        }
    }
}

附注

parseComments中有一个判断式if (!commentList.contains(comment)),会使用这个的理由是在Ptt文章中使用上翻一页时,目前页面的第一行会成为前一页的最後一行,或者前一页的内容不满萤幕高度时,会将目前页面的前几行内容拿来补齐页面,因此会有推文重复的情形。

另外记得我在Day17的Comment data class中有覆写equals方法,主要就是为了判断此情形。

不过这个方法会有一个side effect,就是若有使用者在同一分钟内重复推文两次,也会被判断成重复推文而不加入。但这种状态我认为其实显示两次也没有太大意义,就让它发生好了。

目前画面

https://imgur.com/tmT3eGv.gif


<<:  Day 22网路程序设计

>>:  Day19 Let's ODOO: Logging

Day25-实作

终於到了30天的尾声,该学的都学了! 接下来就是运用在实际的案例上。剩下的这几天我要跟着「重新认识V...

Spring Framework X Kotlin Day 20 Security

GitHub Repo https://github.com/b2etw/Spring-Kotlin...

D29 / Jake 认为 Compose 不是 Compose? - Compose 是什麽

最後几天了,想来聊聊一开始会写这个系列的原因。其实 Compose 在刚出来的时候,就在各大部落格、...

【Day 30】- 结语 : 从 0 开始的网路爬虫

结语   完成了连续一个月的铁人赛了!当初觉得每天发一篇应该不会太难,甚至还在开赛前屯了四篇,结果事...

Day 12:Commitizen

话说今天本来是打算要接着昨天的进度纪录架设 grafana 的 dashboard,可是昨天半夜 d...