Android类似微信聊天页面教程(Kotlin)六——朋友圈

前提条件

安装并配置好Android Studio

Android Studio Hedgehog | 2023.1.1
Build #AI-231.9392.1.2311.11076708, built on November 10, 2023
Runtime version: 17.0.7+0-b2043.56-10550314 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Windows 11.0
GC: G1 Young Generation, G1 Old Generation
Memory: 2048M
Cores: 6
Registry:
    external.system.auto.import.disabled=true
    debugger.new.tool.window.layout=true
    ide.text.editor.with.preview.show.floating.toolbar=false
    ide.experimental.ui=true
    ide.balloon.shadow.size=0

Non-Bundled Plugins:
    GLSL (1.24)
    com.ankit.mahadik.json.dart.class (2.16)
    com.intuit.intellij.makefile (2.2.1)
    com.github.setial (4.0.2)
    GsonOrXmlFormat (2.0)
    Dart (231.9409)
    com.localizely.flutter-intl (1.18.4-2022.2)
    com.tao.getx (3.3.2)
    com.alayouni.ansiHighlight (23.1.0)
    com.likfe.ideaplugin.eventbus3 (2020.0.2)
    com.layernet.plugin.adbwifi (1.0.5)
    com.mistamek.drawablepreview.drawable-preview (1.1.6)
    io.flutter (76.3.3)
    com.ruiyu.ruiyu (5.1.2)

build.gradle(:Project)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
}

 setting.gradle

pluginManagement {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public' }
        google()
        mavenCentral()
        gradlePluginPortal()
        maven { url 'https://jitpack.io' }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public' }
        google()
        mavenCentral()
        gradlePluginPortal()
        maven { url 'https://jitpack.io' }
    }
}
rootProject.name = "FeChat"
include ':app'

build.gralde(:app)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
 
    id 'kotlin-android'
    id 'kotlin-kapt'
}
 
android {
    namespace 'com.example.fechat'
    compileSdk 33
 
    defaultConfig {
        applicationId "com.example.fechat"
        minSdk 26
        targetSdk 33
        versionCode 1
        versionName "1.0"
    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
 
dependencies {
 
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation("androidx.activity:activity-ktx:1.7.1")
 
    // 沉浸式状态栏 https://github.com/gyf-dev/ImmersionBar
    implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
    implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0' // fragment快速实现(可选)
    implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0' // kotlin扩展(可选)
    implementation 'com.google.code.gson:gson:2.8.9'
 
    implementation "androidx.room:room-runtime:2.4.2"
    implementation "androidx.room:room-ktx:2.4.2"
    kapt "androidx.room:room-compiler:2.4.2"
    implementation 'org.apache.commons:commons-csv:1.5'
    implementation 'com.permissionx.guolindev:permissionx:1.4.0'
    implementation 'com.blankj:utilcodex:1.30.0' // 无
    implementation 'com.github.bumptech.glide:glide:4.12.0'
    kapt 'com.github.bumptech.glide:compiler:4.12.0'
    implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.3'
    implementation 'com.github.li-xiaojun:XPopup:latest.release'
}

对Kotlin语言有基本了解

内容在前一篇博客中写了基础配置,如果本篇内容看不懂,可以先去上一篇

AppBarLayout

目标:用于实现顶部图片背景和标题栏,在向上滑动时隐藏背景图片,只显示标题栏;下拉时显示整个背景图。

AppBarLayout 是一个垂直布局LinearLayout,它实现了材料设计应用栏概念的许多功能,即滚动手势。

AppBarLayout.LayoutParams.setScrollFlags(int)孩子们应该通过相关的布局 xml 属性提供他们想要的滚动行为: app:layout_scrollFlags

此视图在很大程度上取决于是否用作CoordinatorLayout. 如果您在不同的 中使用 AppBarLayout ViewGroup,它的大部分功能将无法工作。

AppBarLayout 还需要一个单独的滚动同级,以便知道何时滚动。绑定是通过AppBarLayout.ScrollingViewBehavior行为类完成的,这意味着您应该将滚动视图的行为设置为 的实例AppBarLayout.ScrollingViewBehavior。包含完整类名的字符串资源可用。

<com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:elevation="0dp">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/white"
            android:fitsSystemWindows="true"
            android:minHeight="0dp"
            app:elevation="0dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <ImageView
                android:id="@+id/background"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:scaleType="centerCrop"
                tools:src="@drawable/kenan1"
                app:layout_collapseMode="parallax"
                tools:ignore="ContentDescription" />

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="165dp">

                <TextView
                    android:id="@+id/nameTv"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textColor="@color/white"
                    android:textSize="18sp"
                    android:text="名侦探柯南"
                    android:layout_toStartOf="@+id/headIv"
                    android:layout_marginEnd="10dp"/>

                <ImageView
                    android:id="@+id/headIv"
                    android:layout_width="70dp"
                    android:layout_height="70dp"
                    android:background="@mipmap/ic_ai_user"
                    android:layout_alignParentEnd="true"
                    android:layout_marginEnd="10dp"/>

            </RelativeLayout>

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:contentInsetStart="0dp"
                android:layout_marginTop="20dp">

                <RelativeLayout
                    android:id="@+id/titleLayout"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:background="#00000000">

                    <TextView
                        android:id="@+id/backTv"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="返回"
                        android:textSize="16sp"
                        android:textColor="#000000"
                        android:padding="10dp"
                        android:layout_centerVertical="true"
                        android:layout_marginStart="15dp"/>

                    <TextView
                        android:id="@+id/userName"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="朋友圈"
                        android:textSize="16sp"
                        android:textColor="#000000"
                        android:layout_centerInParent="true"/>

                    <ImageView
                        android:id="@+id/pictureIv"
                        android:layout_width="20dp"
                        android:layout_height="20dp"
                        android:layout_alignParentEnd="true"
                        android:layout_centerVertical="true"
                        android:background="@drawable/icon_camera_light"
                        android:layout_marginEnd="15dp"/>

                </RelativeLayout>
            </androidx.appcompat.widget.Toolbar>

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

标题栏样式切换

滑动隐藏顶部图片时,需要注意标题栏颜色的修改

有图片显示白色文字和图标,没有图片时整个布局颜色为黑色

切换代码如下:

 private fun setWhiteTitle() {
     backTv?.setTextColor(resources.getColor(R.color.white,null))
     userName?.setTextColor(resources.getColor(R.color.white,null))
     pictureIv?.apply {
         Glide.with(this).load(R.drawable.icon_camera_light).into(this)
     }
     ImmersionBar.with(this).transparentStatusBar().statusBarDarkFont(false).init()
 }
 private fun setDarkTitle() {
     backTv?.setTextColor(resources.getColor(R.color.black,null))
     userName?.setTextColor(resources.getColor(R.color.black,null))
     pictureIv?.apply {
         Glide.with(this).load(R.drawable.icon_camera_dark).into(this)
     }
     ImmersionBar.with(this).transparentStatusBar().statusBarDarkFont(true).init()
 }

AppBarLayout是否可滑动 

如果下方没有内容,则禁止AppBarLayout滑动,否则隐藏之后可能无法通过滑动显示

代码实现如下:

private fun banAppBarScroll(isScroll: Boolean) {
    val mAppBarChildAt = appBarLayout!!.getChildAt(0)
    val mAppBarParams = mAppBarChildAt.layoutParams as AppBarLayout.LayoutParams
    if (isScroll) {
        mAppBarParams.scrollFlags =
            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
        mAppBarChildAt.layoutParams = mAppBarParams
    } else {
        mAppBarParams.scrollFlags = 0
    }
}

使用CoordinatorLayout+AppBarLayout时,需要注意下方view需要添加layout_behavior属性

<com.scwang.smartrefresh.layout.SmartRefreshLayout
    android:id="@+id/refreshLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    app:srlEnableLoadMore="false"
    app:srlEnableLoadMoreWhenContentNotFull="true"
    app:srlEnablePreviewInEditMode="true"
    app:srlEnableRefresh="false"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/friendRecycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>

朋友圈适配器的数据机构设计

数据类

package com.example.fechat.bean

import android.graphics.drawable.Drawable

data class FriendData(
    val head: Drawable,
    val nick: String,
    val infoText: String,
    val infoImage: Drawable?,
    val infoLocation: String,
    val infoDate: String
)

head:保存个人头像

nick:保存个人昵称

infoText:保存朋友圈文字

infoImage:保存朋友圈图片或者视频

infoLocation:保存朋友圈定位,可设计成隐藏

infoDate:保存朋友圈发送的时间

测试数据

FriendData(BaseApplication.headers!![0]!!,"用户0", "故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,唯见长江天际流。", null, "上海", "1小时前")

整体代码设计

Activity

package com.example.fechat.activity

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.fechat.R
import com.example.fechat.adapter.DiscoverAdapter
import com.example.fechat.adapter.FriendAdapter
import com.example.fechat.base.BaseApplication
import com.example.fechat.bean.FriendData
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.gyf.immersionbar.ImmersionBar
import com.scwang.smartrefresh.layout.SmartRefreshLayout

class FriendGroupActivity : AppCompatActivity() {

    private var appBarLayout: AppBarLayout? = null
    private var background: ImageView? = null
    private var pictureIv: ImageView? = null
    private var backTv: TextView? = null
    private var userName: TextView? = null
    private var refreshLayout: SmartRefreshLayout? = null
    private var friendRecycler: RecyclerView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ImmersionBar.with(this)
            .transparentStatusBar()
            .statusBarDarkFont(false)
            .navigationBarColor(R.color.white)
            .navigationBarDarkIcon(true)
            .init()
        setContentView(R.layout.activity_friend)
        initTitle()
        initRecyclerView()
    }

    private fun initTitle() {
        appBarLayout = findViewById(R.id.appbar)
        background = findViewById(R.id.background)
        pictureIv = findViewById(R.id.pictureIv)
        backTv = findViewById(R.id.backTv)
        userName = findViewById(R.id.userName)
        appBarLayout?.addOnOffsetChangedListener(offsetChangedListener)
        background?.apply {
            Glide.with(this).load(R.drawable.kenan).into(this)
        }
        banAppBarScroll(false)
    }

    private val offsetChangedListener =
        OnOffsetChangedListener { appBarLayout: AppBarLayout, verticalOffset: Int ->
            if (verticalOffset == 0) {
                setWhiteTitle()
            } else if (Math.abs(verticalOffset) >= appBarLayout.totalScrollRange) {
                setDarkTitle()
            }
        }

    private fun banAppBarScroll(isScroll: Boolean) {
        val mAppBarChildAt = appBarLayout!!.getChildAt(0)
        val mAppBarParams = mAppBarChildAt.layoutParams as AppBarLayout.LayoutParams
        if (isScroll) {
            mAppBarParams.scrollFlags =
                AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
            mAppBarChildAt.layoutParams = mAppBarParams
        } else {
            mAppBarParams.scrollFlags = 0
        }
    }

    private fun setWhiteTitle() {
        backTv?.setTextColor(resources.getColor(R.color.white,null))
        userName?.setTextColor(resources.getColor(R.color.white,null))
        pictureIv?.apply {
            Glide.with(this).load(R.drawable.icon_camera_light).into(this)
        }
        ImmersionBar.with(this).transparentStatusBar().statusBarDarkFont(false).init()
    }

    private fun setDarkTitle() {
        backTv?.setTextColor(resources.getColor(R.color.black,null))
        userName?.setTextColor(resources.getColor(R.color.black,null))
        pictureIv?.apply {
            Glide.with(this).load(R.drawable.icon_camera_dark).into(this)
        }
        ImmersionBar.with(this).transparentStatusBar().statusBarDarkFont(true).init()
    }

    private fun initRecyclerView() {
        refreshLayout = findViewById(R.id.refreshLayout)
        friendRecycler = findViewById(R.id.friendRecycler)
        friendRecycler?.layoutManager = LinearLayoutManager(this)
        val adapter = FriendAdapter(this, getFriendData())
        friendRecycler?.adapter = adapter
        banAppBarScroll(true)
    }

    private fun getFriendData() : ArrayList<FriendData> {
        val friendData = ArrayList<FriendData>()
        val data0 = FriendData(BaseApplication.headers!![0]!!,"用户0", "故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,唯见长江天际流。", null, "上海", "1小时前")
        val data1 = FriendData(BaseApplication.headers!![1]!!,"用户1", "君不见,黄河之水天上来,奔流到海不复回。\n" +
                "君不见,高堂明镜悲白发,朝如青丝暮成雪。\n" +
                "人生得意须尽欢,莫使金樽空对月。\n" +
                "天生我材必有用,千金散尽还复来。\n" +
                "烹羊宰牛且为乐,会须一饮三百杯。\n" +
                "岑夫子,丹丘生,将进酒,杯莫停。\n" +
                "与君歌一曲,请君为我倾耳听。\n" +
                "钟鼓馔玉不足贵,但愿长醉不愿醒。\n" +
                "古来圣贤皆寂寞,惟有饮者留其名。\n" +
                "陈王昔时宴平乐,斗酒十千恣欢谑。\n" +
                "主人何为言少钱,径须沽取对君酌。\n" +
                "五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。", resources?.getDrawableForDensity(R.drawable.kenan1, 0, null), "北京", "2小时前")
        val data2 = FriendData(BaseApplication.headers!![2]!!,"用户2", "渭城朝雨浥轻尘,客舍青青柳色新。劝君更尽一杯酒,西出阳关无故人。", null, "武汉", "13小时前")
        val data3 = FriendData(BaseApplication.headers!![3]!!,"用户3", "泉眼无声惜细流,树阴照水爱晴柔。小荷才露尖尖角,早有蜻蜓立上头。", resources?.getDrawableForDensity(R.drawable.kenan2, 0, null), "南京", "14小时前")
        val data4 = FriendData(BaseApplication.headers!![4]!!,"用户4", "丙辰中秋,欢饮达旦,大醉,作此篇,兼怀子由。明月几时有?把酒问青天。不知天上宫阙,今夕是何年。我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。(何似 一作:何时;又恐 一作:惟 / 唯恐)转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。(长向 一作:偏向)", resources?.getDrawableForDensity(R.drawable.kenan3, 0, null), "成都", "15小时前")
        val data5 = FriendData(BaseApplication.headers!![5]!!,"用户5", "关关雎鸠,在河之洲。窈窕淑女,君子好逑。\n" +
                "参差荇菜,左右流之。窈窕淑女,寤寐求之。\n" +
                "求之不得,寤寐思服。悠哉悠哉,辗转反侧。\n" +
                "参差荇菜,左右采之。窈窕淑女,琴瑟友之。\n" +
                "参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。", resources?.getDrawableForDensity(R.drawable.kenan4, 0, null), "杭州", "16小时前")
        val data6 = FriendData(BaseApplication.headers!![6]!!,"用户6", "山不在高,有仙则名。水不在深,有龙则灵。斯是陋室,惟吾德馨。苔痕上阶绿,草色入帘青。谈笑有鸿儒,往来无白丁。可以调素琴,阅金经。无丝竹之乱耳,无案牍之劳形。南阳诸葛庐,西蜀子云亭。孔子云:何陋之有?", null, "深圳", "17小时前")
        val data7 = FriendData(BaseApplication.headers!![7]!!,"用户7", "大江东去,浪淘尽,千古风流人物。\n" +
                "故垒西边,人道是,三国周郎赤壁。\n" +
                "乱石穿空,惊涛拍岸,卷起千堆雪。(穿空 一作:崩云)\n" +
                "江山如画,一时多少豪杰。\n" +
                "遥想公瑾当年,小乔初嫁了,雄姿英发。\n" +
                "羽扇纶巾,谈笑间,樯橹灰飞烟灭。(樯橹 一作:强虏)\n" +
                "故国神游,多情应笑我,早生华发。\n" +
                "人生如梦,一尊还酹江月。(人生 一作:人间;尊 同:樽)", resources?.getDrawableForDensity(R.drawable.kenan5, 0, null), "广州", "20小时前")
        friendData.add(data0)
        friendData.add(data1)
        friendData.add(data2)
        friendData.add(data3)
        friendData.add(data4)
        friendData.add(data5)
        friendData.add(data6)
        friendData.add(data7)
        return friendData
    }
}

adapter

package com.example.fechat.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.fechat.R
import com.example.fechat.bean.FriendData

class FriendAdapter(private val activity: AppCompatActivity, private val data: List<FriendData>) :
    RecyclerView.Adapter<FriendAdapter.BaseHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.friend_item, parent, false);
        return BaseHolder(view)
    }

    override fun getItemCount(): Int {
        return data.size
    }

    override fun onBindViewHolder(holder: BaseHolder, position: Int) {
        Glide.with(activity).load(data[position].head).into(holder.headTv)
        if (data[position].infoImage == null) {
            holder.smsImage.visibility = View.GONE
        } else {
            holder.smsImage.visibility = View.VISIBLE
            Glide.with(activity).load(data[position].infoImage).override(
                data[position].infoImage?.intrinsicWidth ?: 0,
                data[position].infoImage?.intrinsicHeight ?: 0
            ).fitCenter().into(holder.smsImage)
        }
        holder.userName.text = data[position].nick
        holder.smsLocation.text = data[position].infoLocation
        holder.smsDate.text = data[position].infoDate
        holder.smsInfo.text = data[position].infoText
    }

    class BaseHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val headTv: ImageView = itemView.findViewById(R.id.headTv)
        val userName: TextView = itemView.findViewById(R.id.userName)
        val smsInfo: TextView = itemView.findViewById(R.id.smsInfo)
        val smsImage: ImageView = itemView.findViewById(R.id.smsImage)
        val smsLocation: TextView = itemView.findViewById(R.id.smsLocation)
        val smsDate: TextView = itemView.findViewById(R.id.smsDate)
    }
}

数据结构

package com.example.fechat.bean

import android.graphics.drawable.Drawable

data class FriendData(
    val head: Drawable,
    val nick: String,
    val infoText: String,
    val infoImage: Drawable?,
    val infoLocation: String,
    val infoDate: String
)

开源地址

猜你喜欢

转载自blog.csdn.net/mozushixin_1/article/details/134836600