前言
上一篇文章玩安卓從 0 到 1 之總體概覽感覺沒寫好,果不其然,反響不咋地。當(dāng)然也正常,既沒有好看的頁面(只是最簡單的md文檔)荧库,又因?yàn)楹镁脹]寫文章了,國慶假期也剛過赵刑,有點(diǎn)懵逼分衫,寫的時(shí)候有好多地方想好好說一說但不知道該怎樣描述,于是乎一通粘貼代碼般此。結(jié)果蚪战。。铐懊。
以后寫文章可不能這樣了邀桑,好看的文章不太會(huì)排版,還是盡量注重內(nèi)容吧【影牵現(xiàn)在寫文章不僅僅是自己的筆記了概漱,寫的不對(duì)的話有可能誤人子弟丑慎;不過反過來說喜喂,想要不被誤導(dǎo)直接去看官網(wǎng)不得了,那里雖然又可能也有錯(cuò)誤但畢竟是少數(shù)竿裂。
牢騷發(fā)到此為止玉吁,下面就開始今天的正文吧。
正文
這篇文章說的依然是玩安卓這個(gè) app腻异,按照慣例进副,還是放一下 Github 地址和 apk 下載地址吧。
apk 下載地址:www.pgyer.com/llj2
Github地址:github.com/zhujiang521…
看到標(biāo)題應(yīng)該清楚咱們今天要實(shí)現(xiàn)的項(xiàng)目的首頁悔常,先來看一下實(shí)現(xiàn)好的樣子吧影斑!
[圖片上傳失敗...(image-9d1c06-1602472643702)]
看起來是不是很簡單!結(jié)構(gòu)很清晰机打,最上面是標(biāo)題欄矫户,往下是 Banner ,再往下就是文章列表了残邀,很簡單的一個(gè)首頁皆辽。實(shí)現(xiàn)方式有幾種,要么直接使用 RecyclerView 直接排列下來,要么用 LinearLayout 一個(gè)一個(gè)往下排壁熄,其實(shí)并沒有哪種實(shí)現(xiàn)方式更好敲董,喜歡使用哪種就用哪種不得了!我在這里選擇的使用 LinearLayout 一個(gè)一個(gè)往下排空另,簡單清晰明了盆耽,挺好!
TitleBar 標(biāo)題欄
咱們就一個(gè)一個(gè)來吧扼菠!先來看下 TitleBar 在首頁需要的功能:中間的標(biāo)題征字、右上角的搜索和點(diǎn)擊事件,之前寫過一篇文章就寫的怎樣自定義 TitleBar :構(gòu)建安卓項(xiàng)目通用TitleBar娇豫,有需要的可以看下匙姜。
來看下怎樣使用吧:
<com.zj.core.util.TitleBar
android:id="@+id/homeTitleBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:backImageVisiable="false"
app:titleName="首頁" />
很簡單吧,在布局中可以直接指定標(biāo)題名稱和是否顯示返回按鈕冯痢,這里的首頁明顯不需要返回按鈕氮昧,所以設(shè)置的 false 。咱們來看下在代碼中怎樣設(shè)置這兩個(gè)屬性:
homeTitleBar.setTitle("標(biāo)題")
homeTitleBar.setBackImageVisiable(false)
剛才還提到右上角要顯示一個(gè)搜索的文本并要有點(diǎn)擊事件浦楣,咱們來看下怎樣寫:
homeTitleBar.setRightText("搜索")
homeTitleBar.setRightTextOnClickListener {
// 點(diǎn)擊事件要實(shí)現(xiàn)的邏輯
}
是不是很簡單袖肥,這就完事了,當(dāng)然如果想寫一個(gè)布局每個(gè)頁面進(jìn)行 include 也不是不可以振劳,只是有點(diǎn)麻煩而已椎组,實(shí)現(xiàn)效果其實(shí)是一樣的,沒有什么對(duì)錯(cuò)好壞之分历恐。
Banner
Banner 的話為了簡單省事就直接使用三方庫了寸癌,之前也寫過一篇這個(gè)三方庫的簡單使用,可以參考:安卓實(shí)現(xiàn)Banner輪播圖自定義圖片(非網(wǎng)絡(luò)圖片)弱贼。
三方庫的依賴如下:
implementation 'com.youth.banner:banner:2.1.0'
寫一下使用吧:
<com.youth.banner.Banner
android:id="@+id/homeBanner"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_180" />
布局很簡單蒸苇,直接寫上就可以了。代碼也不難吮旅,只需要寫一個(gè)適配器然后放進(jìn)去設(shè)置開始即可溪烤。
先寫一個(gè)適配器吧:
open class ImageAdapter(private val mContext: Context, mData: List<BannerBean>) :
BannerAdapter<BannerBean?, ImageAdapter.BannerViewHolder?>(mData) {
override fun onCreateHolder(parent: ViewGroup,viewType: Int): BannerViewHolder {
val imageView = ImageView(parent.context)
imageView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
return BannerViewHolder(imageView)
}
class BannerViewHolder(view: ImageView) :
RecyclerView.ViewHolder(view) {
var imageView: ImageView = view
}
override fun onBindView(holder: BannerViewHolder?,data: BannerBean?,
position: Int,size: Int) {
Glide.with(mContext).load(if (data?.filePath == null) data?.imagePath else data.filePath).into(holder!!.imageView)
}
}
二十來行代碼,核心就是通過 Glide 加載一下圖片庇勃。
上面說了檬嘀,要把適配器放進(jìn)去,放到哪呢责嚷?當(dāng)然是 Banner 中了:
val bannerAdapter = ImageAdapter(context!!, viewModel.bannerList)
homeBanner.adapter = bannerAdapter
// 設(shè)置為圓形指示器并開始
homeBanner.setIndicator(CircleIndicator(context)).start()
到這里就差不多了鸳兽,但是為了避免內(nèi)存泄露和提升性能,需要在 onResume 頁面可見的時(shí)候開始滾動(dòng)再层,在 onPause 頁面不可見的時(shí)候停止?jié)L動(dòng):
override fun onResume() {
super.onResume()
homeBanner.start()
}
override fun onPause() {
super.onPause()
homeBanner.stop()
}
RecyclerView
排到這里就該用 RecyclerView 來展示文章了贸铜,這個(gè)布局就不貼了堡纬,太簡單了,但想了想還是需要貼一下蒿秦,因?yàn)檫@里需要有下拉刷新和上拉加載烤镐,這里用到了一個(gè)三方庫,大家應(yīng)該都不陌生棍鳖,下面是依賴:
implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.2'
接下來是布局使用方法:
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/homeSmartRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/homeRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
RecyclerView 的適配器我用的是泓洋大神的開源庫炮叶,下面是依賴:
api 'com.zhy:base-rvadapter:3.0.3'
先來看一下下拉刷新和上拉加載怎么使用吧:
homeSmartRefreshLayout.apply {
setOnRefreshListener { reLayout ->
reLayout.finishRefresh(measureTimeMillis {
page = 1
getArticleList(true)
}.toInt())
}
setOnLoadMoreListener { reLayout ->
val time = measureTimeMillis {
page++
getArticleList(true)
}.toInt()
reLayout.finishLoadMore(if (time > 1000) time else 1000)
}
}
通過回調(diào)的名稱也能猜出來怎么調(diào)用。
適配器的代碼就不往上貼了渡处,大家感興趣的話可以去上面 Github 中下載代碼看镜悉。
OK了,首頁布局這就完成了医瘫,接下來就該獲取數(shù)據(jù)了侣肄!
獲取數(shù)據(jù)
Banner數(shù)據(jù)
這里需要思考一下,Banner 數(shù)據(jù)是否會(huì)實(shí)時(shí)更新醇份,據(jù)我所知稼锅,Banner 數(shù)據(jù)最多一周更新一回,目前市面上的玩安卓 app 大部分都是每次進(jìn)入都會(huì)請求下網(wǎng)絡(luò)僚纷,然后再重新加載矩距,這樣無疑會(huì)增加網(wǎng)絡(luò)成本,更何況都是圖片怖竭,會(huì)消耗用戶的 money 啊锥债,雖然說現(xiàn)在流量不值錢,但也需要盡可能地省叭簟哮肚!
所以這里的 Banner 數(shù)據(jù)我進(jìn)行了一些操作,先去本地?cái)?shù)據(jù)庫中查看是否有 Banner 的數(shù)據(jù)趣兄,如果有绽左,并且和上回刷新的時(shí)間間隔在一天以內(nèi)(這里為了預(yù)防更新,所以暫定的一天)艇潭,那么就把數(shù)據(jù)庫中的數(shù)據(jù)返回,如果沒有數(shù)據(jù)或者和上回刷新的時(shí)間間隔在一天以外的話就去請求網(wǎng)絡(luò)數(shù)據(jù)戏蔑,請求網(wǎng)絡(luò)數(shù)據(jù)又分為成功或失敗蹋凝,如果失敗則返回失敗信息,頁面進(jìn)行顯示對(duì)應(yīng)頁面总棵;如果成功則記錄下當(dāng)前的時(shí)間鳍寂,如果數(shù)據(jù)庫查出來有數(shù)據(jù)并且和請求到的數(shù)據(jù)一樣,那么就還返回?cái)?shù)據(jù)庫中的數(shù)據(jù)情龄;反之迄汛,則把數(shù)據(jù)庫中的 Banner 數(shù)據(jù)刪除捍壤,并且把請求到的數(shù)據(jù)插入到數(shù)據(jù)庫中,然后把數(shù)據(jù)返回鞍爱。
這里說的有點(diǎn)繞鹃觉,我還是畫個(gè)圖給大家看看吧,好理解一些:
[圖片上傳失敗...(image-a0e8eb-1602472643703)]
沒怎么畫過類似的流程圖睹逃,之前畫過都是在大學(xué)的時(shí)候盗扇,平時(shí)話也就是在本上隨便畫畫,畫的不好或者不對(duì)的地方各位多多包涵沉填!大概說下這張圖疗隶,意思其實(shí)和上面那段話描述的意思一致,從中間粉色的 查看本地?cái)?shù)據(jù)庫 開始翼闹,分為各種情況下的數(shù)據(jù)獲取方式斑鼻。
好了,說了這么多又畫了這么多猎荠,是時(shí)候看看代碼實(shí)現(xiàn)了:
fun getBanner(application: Application) = fire {
val spUtils = SPUtils.getInstance()
val downImageTime by Preference(DOWN_IMAGE_TIME, System.currentTimeMillis())
val bannerBeanDao = PlayDatabase.getDatabase(application).bannerBeanDao()
val bannerBeanList = bannerBeanDao.getBannerBeanList()
if (bannerBeanList.isNotEmpty() && downImageTime > 0 && downImageTime - System.currentTimeMillis() < ONE_DAY) {
Result.success(bannerBeanList)
} else {
val bannerResponse = PlayAndroidNetwork.getBanner()
if (bannerResponse.errorCode == 0) {
val bannerList = bannerResponse.data
spUtils.put(DOWN_IMAGE_TIME, System.currentTimeMillis())
if (bannerBeanList.isNotEmpty() && bannerBeanList[0].url == bannerList[0].url) {
Result.success(bannerBeanList)
} else {
bannerBeanDao.deleteAll()
insertBannerList(application, bannerBeanDao, bannerList)
Result.success(bannerList)
}
} else {
Result.failure(RuntimeException("response status is ${bannerResponse.errorCode} msg is ${bannerResponse.errorMsg}"))
}
}
}
其實(shí)剛才的邏輯如果看懂了的話這段代碼應(yīng)該看著很簡單卵沉,就是按照上面的邏輯來寫的,對(duì)了法牲,插入數(shù)據(jù)庫的 insertBannerList 方法還沒寫:
private suspend fun insertBannerList(
application: Application,
bannerBeanDao: BannerBeanDao,
bannerList: List<BannerBean>
) {
bannerList.forEach {
val file = Glide.with(application)
.load(it.imagePath)
.downloadOnly(SIZE_ORIGINAL, SIZE_ORIGINAL)
.get()
it.filePath = file.absolutePath
bannerBeanDao.insert(it)
}
}
代碼都很簡單史汗,重要的是這塊的思路,接下來該看文章列表的數(shù)據(jù)了拒垃。
文章列表數(shù)據(jù)
文章的數(shù)據(jù)獲取其實(shí)和 Banner 差不多停撞,邏輯基本一樣,都是從數(shù)據(jù)庫中讀取文件悼瓮,然后判斷是否需要刷新戈毒,這塊的時(shí)間改為了四小時(shí),因?yàn)槲恼驴赡芤恢痹诟潞岜ぃ匀×藗€(gè)比較小的值埋市,可以根據(jù)需求自己來定義。文章列表的數(shù)據(jù)和 Banner 的不同之處在于文章列表需要請求兩次命贴,需要判斷當(dāng)前是第幾頁道宅,如果是第 0 頁的話需要把置頂?shù)奈恼绿砑拥阶钋懊妫绻皇堑?0 頁的話則只需要把后面的文章添加上胸蛛。
這里的實(shí)現(xiàn)其實(shí)我偷懶了污茵,但也不算是偷懶。葬项。泞当。為什么這樣說呢?因?yàn)槲抑痪彺媪说谝豁摰奈恼铝斜頂?shù)據(jù)民珍,但其實(shí)并不是偷懶襟士,因?yàn)槲恼铝斜頂?shù)據(jù)不定盗飒,可能更新頻率很快,緩存了太多頁的數(shù)據(jù)到后來又需要全部更新陋桂,亦或者全部刪除再重新插入逆趣,得不償失。緩存第一頁的數(shù)據(jù)在于用戶之前已經(jīng)打開過項(xiàng)目了章喉,數(shù)據(jù)也都正常顯示汗贫,如果突然沒網(wǎng)了,再次重新打開應(yīng)用不至于大白頁或者顯示沒有網(wǎng)絡(luò)秸脱,顯示出緩存的數(shù)據(jù)比較優(yōu)雅落包,如果用戶下拉刷新或者上拉加載的話提醒用戶當(dāng)前沒有網(wǎng)絡(luò)即可。
既然 Banner 的數(shù)據(jù)都畫了個(gè)圖摊唇,那么文章列表的數(shù)據(jù)也得來畫一個(gè)咐蝇!
[圖片上傳失敗...(image-e855e1-1602472643703)]
這張圖其實(shí)有偷懶了,這里判斷完是否為第一頁之后還要依次判斷置頂文章和列表文章巷查,根據(jù)數(shù)據(jù)庫的數(shù)據(jù)和網(wǎng)絡(luò)請求數(shù)據(jù)是否一致來判斷是否更新數(shù)據(jù)庫的數(shù)據(jù)有序,再將數(shù)據(jù)返回到 ViewModel。
來吧岛请,看下代碼吧旭寿,這塊代碼有點(diǎn)多,我只展示下大概的邏輯吧崇败,如果想看完整代碼盅称,還是去 Github 上直接下載代碼就行:
fun getArticleList(application: Application, query: QueryHomeArticle) = fire {
coroutineScope {
val res = arrayListOf<Article>()
if (query.page == 1) {
val spUtils = SPUtils.getInstance()
val downArticleTime by Preference(DOWN_ARTICLE_TIME, System.currentTimeMillis())
val articleListDao = PlayDatabase.getDatabase(application).browseHistoryDao()
val articleListTop = articleListDao.getTopArticleList(HOME_TOP)
val downTopArticleTime by Preference(
DOWN_TOP_ARTICLE_TIME,
System.currentTimeMillis()
)
if (articleListTop.isNotEmpty() && downTopArticleTime > 0 &&
downTopArticleTime - System.currentTimeMillis() < FOUR_HOUR && !query.isRefresh
) {
res.addAll(articleListTop)
} else {
val topArticleListDeferred =
async { PlayAndroidNetwork.getTopArticleList() }
val topArticleList = topArticleListDeferred.await()
if (topArticleList.errorCode == 0) {
if (articleListTop.isNotEmpty() && articleListTop[0].link == topArticleList.data[0].link && !query.isRefresh) {
res.addAll(articleListTop)
} else {
res.addAll(topArticleList.data)
topArticleList.data.forEach {
it.localType = HOME_TOP
}
spUtils.put(DOWN_TOP_ARTICLE_TIME, System.currentTimeMillis())
articleListDao.deleteAll(HOME_TOP)
articleListDao.insertList(topArticleList.data)
}
}
}
} else {
val articleListDeferred =
async { PlayAndroidNetwork.getArticleList(query.page - 1) }
val articleList = articleListDeferred.await()
if (articleList.errorCode == 0) {
res.addAll(articleList.data.datas)
Result.success(res)
} else {
Result.failure(
RuntimeException(
"response status is ${articleList.errorCode}" + " msg is ${articleList.errorMsg}"
)
)
}
}
}
}
大家可以看到我只展示了置頂文章的數(shù)據(jù)緩存,文章列表的原理一樣后室,就不贅述了缩膝。
再放一下這幾個(gè)模塊用到的常量吧:
const val ONE_DAY = 1000 * 60 * 60 * 24
const val FOUR_HOUR = 1000 * 60 * 60 * 4
const val DOWN_IMAGE_TIME = "DownImageTime"
const val DOWN_TOP_ARTICLE_TIME = "DownTopArticleTime"
const val DOWN_ARTICLE_TIME = "DownArticleTime"
const val DOWN_PROJECT_ARTICLE_TIME = "DownProjectArticleTime"
const val DOWN_OFFICIAL_ARTICLE_TIME = "DownOfficialArticleTime"
ViewModel
都寫的差不多了就該 ViewModel 登場了,ViewModel 的代碼比較簡單岸霹,我直接放上疾层,然后下面簡單描述下吧:
class HomePageViewModel(application: Application) : AndroidViewModel(application) {
private val pageLiveData = MutableLiveData<QueryHomeArticle>()
private val refreshLiveData = MutableLiveData<Boolean>()
val bannerList = ArrayList<BannerBean>()
val articleList = ArrayList<Article>()
val articleLiveData = Transformations.switchMap(pageLiveData) { query ->
HomeRepository.getArticleList(application, query)
}
val bannerLiveData = Transformations.switchMap(refreshLiveData) { isRefresh ->
HomeRepository.getBanner(application,isRefresh)
}
fun getBanner(isRefresh: Boolean) {
refreshLiveData.value = isRefresh
}
fun getArticleList(page: Int, isRefresh: Boolean) {
pageLiveData.value = QueryHomeArticle(page, isRefresh)
}
}
data class QueryHomeArticle(var page: Int, var isRefresh: Boolean)
和上一篇文章一樣,同樣用的是 AndroidViewModel 贡避,因?yàn)樵?Repository 中需要用到數(shù)據(jù)庫痛黎,所以要使用。
ViewModel 中的邏輯很清晰贸桶,定義兩個(gè) ArrayList 來存放數(shù)據(jù)舅逸,使用 LiveData 來觀察數(shù)據(jù)的改變,兩個(gè) get 方法來調(diào)動(dòng)方法執(zhí)行以改變數(shù)據(jù)皇筛。
橫豎屏適配
到這里應(yīng)該整個(gè)邏輯都理通了,代碼應(yīng)該也都寫的差不多了坠七,那么來運(yùn)行下看看吧水醋!
運(yùn)行之后發(fā)現(xiàn)豎屏運(yùn)行顯示正常旗笔,但當(dāng)橫屏顯示的時(shí)候,頁面完全無法正常使用拄踪!Banner 基本上把所有的空間都占了蝇恶,文章列表根本無法進(jìn)行使用!
這個(gè)時(shí)候就需要橫豎屏適配了惶桐,其實(shí)橫豎屏適配很簡單撮弧,只需要在 res 目錄下建立一個(gè) layout-land 的文件夾,把橫屏的布局放入進(jìn)去即可姚糊,和豎屏布局的名稱一樣就行贿衍。
在這里大家可以根據(jù)需求來重新擺放橫屏布局的控件位置。我在這里將屏幕分為兩半救恨,左邊用來顯示 Banner 贸辈,右面用來顯示文章列表,大家來簡單看下布局:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.youth.banner.Banner
android:id="@+id/homeBanner"
android:layout_width="0dp"
android:layout_weight="1.5"
android:layout_height="match_parent" />
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/homeSmartRefreshLayout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/homeRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
</LinearLayout>
完成之后變?yōu)槿缦聵邮匠Σ郏遣皇潜葎偛藕每吹亩嗲嬗伲腋尤菀撞僮髁恕?/p>
[圖片上傳失敗...(image-21b0fa-1602472643703)]
差不多就先寫到這里吧,其他的下一篇文章再來秸仙!
總結(jié)
每次覺得沒多少東西的地方寫著寫著就寫多了嘴拢,每回想著要好好寫的東西卻死活不知道如何下手寫。這一篇文章簡單走了一遍一個(gè)應(yīng)用首頁的簡單實(shí)現(xiàn)邏輯寂纪,并帶給大家橫豎屏的簡單實(shí)現(xiàn)席吴。
寫著寫著就過了午夜12點(diǎn)了,好久沒有寫到這么晚了弊攘,是自己這個(gè)程序員當(dāng)?shù)奶环Q職了抢腐,也好久沒努力地去學(xué)習(xí)了,連續(xù)好久沒有進(jìn)行主動(dòng)學(xué)習(xí)襟交,基本一直處于被動(dòng)學(xué)習(xí)的局面迈倍,不能這樣,自己要加油捣域。
努力啼染,共勉。