iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0

今天來做顯示推文和換頁讀取更多推文的部分。

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 = this@PreviewFragment.adapter
}

主要就是使用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


上一篇
Day18 - 使用ViewBinding取代Kotlin Android Extension
下一篇
Day20 - 更新推文及衝突
系列文
花30天做個Android小專案30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言