Merge pull request #85 from H-PixelDroid/scroll_load
Load images in advance, infinite scrolling
This commit is contained in:
commit
5372e7e9ee
|
@ -94,6 +94,8 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:2.1.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,8 @@ class AppDatabaseTest {
|
|||
@Test
|
||||
fun testUtilsLRU() {
|
||||
for(i in 1..db!!.MAX_NUMBER_OF_POSTS) {
|
||||
//sleep a bit to not have the weird concurrency bugs?
|
||||
Thread.sleep(10)
|
||||
DatabaseUtils.insertAllPosts(db!!, PostEntity(i, i.toString(), date= Calendar.getInstance().time))
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -44,7 +44,7 @@ interface PixelfedAPI {
|
|||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("min_id") min_id: String? = null,
|
||||
@Query("limit") limit: Int? = null
|
||||
@Query("limit") limit: String? = null
|
||||
): Call<List<Status>>
|
||||
|
||||
|
||||
|
@ -55,7 +55,7 @@ interface PixelfedAPI {
|
|||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("min_id") min_id: String? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("limit") limit: String? = null,
|
||||
@Query("local") local: Boolean? = null
|
||||
): Call<List<Status>>
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import android.view.ViewGroup
|
|||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import kotlinx.android.synthetic.main.post_fragment.view.*
|
||||
|
||||
|
||||
class PostFragment : Fragment() {
|
||||
|
@ -18,7 +20,18 @@ class PostFragment : Fragment() {
|
|||
): View? {
|
||||
val status = arguments?.getSerializable(POST_TAG) as Status?
|
||||
val root = inflater.inflate(R.layout.post_fragment, container, false)
|
||||
status?.setupPost(this, root)
|
||||
status?.setupPost(root)
|
||||
//Setup post and profile images
|
||||
ImageConverter.setImageViewFromURL(
|
||||
this,
|
||||
status?.getPostUrl(),
|
||||
root.postPicture
|
||||
)
|
||||
ImageConverter.setImageViewFromURL(
|
||||
this,
|
||||
status?.getProfilePicUrl(),
|
||||
root.profilePic
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
|
|
|
@ -9,21 +9,31 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import androidx.paging.ItemKeyedDataSource
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.bumptech.glide.ListPreloader.PreloadModelProvider
|
||||
import com.h.pixeldroid.BuildConfig
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.objects.FeedContent
|
||||
import kotlinx.android.synthetic.main.fragment_feed.*
|
||||
import kotlinx.android.synthetic.main.fragment_feed.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
|
||||
open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment() {
|
||||
|
||||
var content : List<T> = ArrayList()
|
||||
lateinit var content: LiveData<PagedList<T>>
|
||||
lateinit var factory: FeedDataSourceFactory
|
||||
|
||||
protected var accessToken: String? = null
|
||||
protected lateinit var pixelfedAPI: PixelfedAPI
|
||||
|
@ -33,34 +43,13 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
|
|||
protected lateinit var adapter : FeedsRecyclerViewAdapter<T, VH>
|
||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
|
||||
protected fun doRequest(call: Call<List<T>>){
|
||||
call.enqueue(object : Callback<List<T>> {
|
||||
override fun onResponse(call: Call<List<T>>, response: Response<List<T>>) {
|
||||
if (response.code() == 200) {
|
||||
val notifications = response.body()!! as ArrayList<T>
|
||||
setContent(notifications)
|
||||
} else{
|
||||
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<T>>, t: Throwable) {
|
||||
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
|
||||
Log.e("FeedFragment", t.toString())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
protected fun setContent(content : ArrayList<T>) {
|
||||
this.content = content
|
||||
adapter.initializeWith(content)
|
||||
}
|
||||
|
||||
|
||||
protected fun onCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_feed, container, false)
|
||||
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
|
||||
list = swipeRefreshLayout.list
|
||||
// Set the adapter
|
||||
|
@ -69,7 +58,6 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
|
|||
return view
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
@ -80,19 +68,88 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
|
|||
pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
|
||||
accessToken = preferences.getString("accessToken", "")
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
//by invalidating data, loadInitial will be called again
|
||||
factory.liveData.value!!.invalidate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class FeedDataSource(private val makeInitialCall: (Int) -> Call<List<T>>,
|
||||
private val makeAfterCall: (Int, String) -> Call<List<T>>
|
||||
): ItemKeyedDataSource<String, T>() {
|
||||
|
||||
//We use the id as the key
|
||||
override fun getKey(item: T): String {
|
||||
return item.id
|
||||
}
|
||||
//This is called to initialize the list, so we want some of the latest statuses
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<T>
|
||||
) {
|
||||
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
|
||||
}
|
||||
|
||||
//This is called to when we get to the bottom of the loaded content, so we want statuses
|
||||
//older than the given key (params.key)
|
||||
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<T>) {
|
||||
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
|
||||
}
|
||||
|
||||
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<T>) {
|
||||
//do nothing here, it is expected to pull to refresh to load newer notifications
|
||||
}
|
||||
|
||||
private fun enqueueCall(call: Call<List<T>>, callback: LoadCallback<T>){
|
||||
call.enqueue(object : Callback<List<T>> {
|
||||
override fun onResponse(call: Call<List<T>>, response: Response<List<T>>) {
|
||||
if (response.code() == 200) {
|
||||
val notifications = response.body()!! as ArrayList<T>
|
||||
callback.onResult(notifications as List<T>)
|
||||
} else{
|
||||
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<T>>, t: Throwable) {
|
||||
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
|
||||
Log.e("FeedFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
inner class FeedDataSourceFactory(
|
||||
private val makeInitialCall: (Int) -> Call<List<T>>,
|
||||
private val makeAfterCall: (Int, String) -> Call<List<T>>
|
||||
): DataSource.Factory<String, T>() {
|
||||
lateinit var liveData: MutableLiveData<FeedDataSource>
|
||||
|
||||
override fun create(): DataSource<String, T> {
|
||||
val dataSource = FeedDataSource(::makeInitialCall.get(), ::makeAfterCall.get())
|
||||
liveData = MutableLiveData()
|
||||
liveData.postValue(dataSource)
|
||||
return dataSource
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FeedsRecyclerViewAdapter<T, VH : RecyclerView.ViewHolder?>: RecyclerView.Adapter<VH>() {
|
||||
abstract class FeedsRecyclerViewAdapter<T: FeedContent, VH : RecyclerView.ViewHolder?>: PagedListAdapter<T, VH>(
|
||||
object : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.id === newItem.id
|
||||
}
|
||||
|
||||
protected val feedContent: ArrayList<T> = arrayListOf()
|
||||
protected lateinit var context: Context
|
||||
|
||||
override fun getItemCount(): Int = feedContent.size
|
||||
|
||||
open fun initializeWith(content: List<T>){
|
||||
feedContent.clear()
|
||||
feedContent.addAll(content)
|
||||
notifyDataSetChanged()
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
), PreloadModelProvider<T> {
|
||||
|
||||
protected lateinit var context: Context
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,143 @@
|
|||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import retrofit2.Call
|
||||
|
||||
|
||||
class HomeFragment : FeedFragment<Status, HomeRecyclerViewAdapter.ViewHolder>() {
|
||||
class HomeFragment : FeedFragment<Status, HomeFragment.HomeRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
lateinit var picRequest: RequestBuilder<Drawable>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container)
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
content = makeContent()
|
||||
|
||||
//RequestBuilder that is re-used for every image
|
||||
picRequest = Glide.with(this)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(ColorDrawable(Color.GRAY))
|
||||
|
||||
adapter = HomeRecyclerViewAdapter()
|
||||
list.adapter = adapter
|
||||
|
||||
content.observe(viewLifecycleOwner,
|
||||
Observer { c ->
|
||||
adapter.submitList(c)
|
||||
//after a refresh is done we need to stop the pull to refresh spinner
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
})
|
||||
|
||||
//Make Glide be aware of the recyclerview and pre-load images
|
||||
val sizeProvider: ListPreloader.PreloadSizeProvider<Status> = ViewPreloadSizeProvider()
|
||||
val preloader: RecyclerViewPreloader<Status> = RecyclerViewPreloader(
|
||||
Glide.with(this), adapter, sizeProvider, 4
|
||||
)
|
||||
list.addOnScrollListener(preloader)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
val call = pixelfedAPI.timelineHome("Bearer $accessToken")
|
||||
doRequest(call)
|
||||
private fun makeContent(): LiveData<PagedList<Status>> {
|
||||
fun makeInitialCall(requestedLoadSize: Int): Call<List<Status>> {
|
||||
return pixelfedAPI
|
||||
.timelineHome("Bearer $accessToken", limit="$requestedLoadSize")
|
||||
}
|
||||
fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Status>> {
|
||||
return pixelfedAPI
|
||||
.timelineHome("Bearer $accessToken", max_id=key,
|
||||
limit="$requestedLoadSize")
|
||||
}
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that can display a list of Statuses
|
||||
*/
|
||||
inner class HomeRecyclerViewAdapter: FeedsRecyclerViewAdapter<Status, HomeRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.post_fragment, parent, false)
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the different elements of the Post Model to the view holder
|
||||
*/
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val post = getItem(position) ?: return
|
||||
val metrics = context.resources.displayMetrics
|
||||
//Limit the height of the different images
|
||||
holder.profilePic?.maxHeight = metrics.heightPixels
|
||||
holder.postPic.maxHeight = metrics.heightPixels
|
||||
|
||||
//Set the two images
|
||||
ImageConverter.setRoundImageFromURL(
|
||||
holder.postView,
|
||||
post.getProfilePicUrl(),
|
||||
holder.profilePic!!
|
||||
)
|
||||
|
||||
picRequest.load(post.getPostUrl()).into(holder.postPic)
|
||||
|
||||
//Set the image back to a placeholder if the original is too big
|
||||
if(holder.postPic.height > metrics.heightPixels) {
|
||||
ImageConverter.setDefaultImage(holder.postView, holder.postPic)
|
||||
}
|
||||
|
||||
//Set the the text views
|
||||
post.setupPost(holder.postView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the posts that will be contained within the feed
|
||||
*/
|
||||
inner class ViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
|
||||
val profilePic : ImageView? = postView.findViewById(R.id.profilePic)
|
||||
val postPic : ImageView = postView.findViewById(R.id.postPicture)
|
||||
val username : TextView = postView.findViewById(R.id.username)
|
||||
val usernameDesc: TextView = postView.findViewById(R.id.usernameDesc)
|
||||
val description : TextView = postView.findViewById(R.id.description)
|
||||
val nlikes : TextView = postView.findViewById(R.id.nlikes)
|
||||
val nshares : TextView = postView.findViewById(R.id.nshares)
|
||||
}
|
||||
|
||||
override fun getPreloadItems(position: Int): MutableList<Status> {
|
||||
val status = getItem(position) ?: return mutableListOf()
|
||||
return mutableListOf(status)
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: Status): RequestBuilder<*>? {
|
||||
return picRequest.load(item.getPostUrl())
|
||||
}
|
||||
val call = pixelfedAPI.timelineHome("Bearer $accessToken")
|
||||
doRequest(call)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.ImageConverter.Companion.setDefaultImage
|
||||
import com.h.pixeldroid.utils.ImageConverter.Companion.setImageViewFromURL
|
||||
import com.h.pixeldroid.utils.ImageConverter.Companion.setRoundImageFromURL
|
||||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that can display a list of Posts
|
||||
*/
|
||||
class HomeRecyclerViewAdapter() : FeedsRecyclerViewAdapter<Status, HomeRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.post_fragment, parent, false)
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the different elements of the Post Model to the view holder
|
||||
*/
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val post = feedContent[position]
|
||||
val metrics = DisplayMetrics()
|
||||
|
||||
//Limit the height of the different images
|
||||
holder.profilePic?.maxHeight = metrics.heightPixels
|
||||
holder.postPic.maxHeight = metrics.heightPixels
|
||||
|
||||
//Set the two images
|
||||
setRoundImageFromURL(holder.postView, post.getProfilePicUrl(), holder.profilePic!!)
|
||||
setImageViewFromURL(holder.postView, post.getPostUrl(), holder.postPic)
|
||||
|
||||
//Set the image back to a placeholder if the original is too big
|
||||
if(holder.postPic.height > metrics.heightPixels) {
|
||||
setDefaultImage(holder.postView, holder.postPic)
|
||||
}
|
||||
|
||||
//Set the the text views
|
||||
holder.username.text = post.getUsername()
|
||||
holder.username.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
holder.usernameDesc.text = post.getUsername()
|
||||
holder.usernameDesc.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
holder.description.text = post.getDescription()
|
||||
|
||||
holder.nlikes.text = post.getNLikes()
|
||||
holder.nlikes.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
holder.nshares.text = post.getNShares()
|
||||
holder.nshares.setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = feedContent.size
|
||||
|
||||
/**
|
||||
* Represents the posts that will be contained within the feed
|
||||
*/
|
||||
inner class ViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
|
||||
val profilePic : ImageView? = postView.findViewById(R.id.profilePic)
|
||||
val postPic : ImageView = postView.findViewById(R.id.postPicture)
|
||||
val username : TextView = postView.findViewById(R.id.username)
|
||||
val usernameDesc: TextView = postView.findViewById(R.id.usernameDesc)
|
||||
val description : TextView = postView.findViewById(R.id.description)
|
||||
val nlikes : TextView = postView.findViewById(R.id.nlikes)
|
||||
val nshares : TextView = postView.findViewById(R.id.nshares)
|
||||
}
|
||||
}
|
|
@ -1,44 +1,199 @@
|
|||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider
|
||||
import com.h.pixeldroid.PostActivity
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import kotlinx.android.synthetic.main.fragment_feed.*
|
||||
import kotlinx.android.synthetic.main.fragment_notifications.view.*
|
||||
import retrofit2.Call
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a list of Items.
|
||||
*/
|
||||
class NotificationsFragment : FeedFragment<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.NotificationsRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
lateinit var profilePicRequest: RequestBuilder<Drawable>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
val view = super.onCreateView(inflater, container)
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
content = makeContent()
|
||||
|
||||
//RequestBuilder that is re-used for every image
|
||||
profilePicRequest = Glide.with(this)
|
||||
.asDrawable().apply(RequestOptions().circleCrop())
|
||||
.placeholder(R.drawable.ic_default_user)
|
||||
|
||||
|
||||
adapter = NotificationsRecyclerViewAdapter()
|
||||
list.adapter = adapter
|
||||
|
||||
content.observe(viewLifecycleOwner,
|
||||
Observer { c ->
|
||||
adapter.submitList(c)
|
||||
//after a refresh is done we need to stop the pull to refresh spinner
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
})
|
||||
|
||||
//Make Glide be aware of the recyclerview and pre-load images
|
||||
val sizeProvider: ListPreloader.PreloadSizeProvider<Notification> = ViewPreloadSizeProvider()
|
||||
val preloader: RecyclerViewPreloader<Notification> = RecyclerViewPreloader(
|
||||
Glide.with(this), adapter, sizeProvider, 4
|
||||
)
|
||||
list.addOnScrollListener(preloader)
|
||||
|
||||
return view
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
val call = pixelfedAPI.notifications("Bearer $accessToken", min_id="1")
|
||||
doRequest(call)
|
||||
private fun makeContent(): LiveData<PagedList<Notification>> {
|
||||
fun makeInitialCall(requestedLoadSize: Int): Call<List<Notification>> {
|
||||
return pixelfedAPI
|
||||
.notifications("Bearer $accessToken", min_id="1", limit="$requestedLoadSize")
|
||||
}
|
||||
val call = pixelfedAPI.notifications("Bearer $accessToken", min_id="1")
|
||||
doRequest(call)
|
||||
fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Notification>> {
|
||||
return pixelfedAPI
|
||||
.notifications("Bearer $accessToken", max_id=key, limit="$requestedLoadSize")
|
||||
}
|
||||
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that can display a [Notification]
|
||||
*/
|
||||
inner class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
private val mOnClickListener: View.OnClickListener
|
||||
|
||||
init {
|
||||
mOnClickListener = View.OnClickListener { v ->
|
||||
val notification = v.tag as Notification
|
||||
openActivity(notification)
|
||||
}
|
||||
}
|
||||
private fun openActivity(notification: Notification){
|
||||
val intent: Intent
|
||||
when (notification.type){
|
||||
Notification.NotificationType.mention, Notification.NotificationType.favourite-> {
|
||||
intent = Intent(context, PostActivity::class.java)
|
||||
intent.putExtra(Status.POST_TAG, notification.status)
|
||||
}
|
||||
Notification.NotificationType.reblog-> {
|
||||
Toast.makeText(context,"Can't see shares yet, sorry!", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
Notification.NotificationType.follow -> {
|
||||
val url = notification.status?.url ?: notification.account.url
|
||||
intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
}
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.fragment_notifications, parent, false)
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val notification = getItem(position) ?: return
|
||||
profilePicRequest.load(notification.account.avatar_static).into(holder.avatar)
|
||||
|
||||
val previewUrl = notification.status?.media_attachments?.getOrNull(0)?.preview_url
|
||||
if(!previewUrl.isNullOrBlank()){
|
||||
Glide.with(holder.mView).load(previewUrl)
|
||||
.placeholder(R.drawable.ic_picture_fallback).into(holder.photoThumbnail)
|
||||
} else{
|
||||
holder.photoThumbnail.visibility = View.GONE
|
||||
}
|
||||
|
||||
setNotificationType(notification.type, notification.account.username, holder.notificationType)
|
||||
|
||||
holder.postDescription.text = notification.status?.content ?: ""
|
||||
|
||||
|
||||
with(holder.mView) {
|
||||
tag = notification
|
||||
setOnClickListener(mOnClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotificationType(type: Notification.NotificationType, username: String,
|
||||
textView: TextView
|
||||
){
|
||||
val context = textView.context
|
||||
val (format: String, drawable: Drawable?) = when(type) {
|
||||
Notification.NotificationType.follow -> {
|
||||
setNotificationTypeTextView(context, R.string.followed_notification, R.drawable.ic_follow)
|
||||
}
|
||||
Notification.NotificationType.mention -> {
|
||||
setNotificationTypeTextView(context, R.string.mention_notification, R.drawable.ic_apenstaart)
|
||||
}
|
||||
|
||||
Notification.NotificationType.reblog -> {
|
||||
setNotificationTypeTextView(context, R.string.shared_notification, R.drawable.ic_share)
|
||||
}
|
||||
|
||||
Notification.NotificationType.favourite -> {
|
||||
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_heart)
|
||||
}
|
||||
}
|
||||
textView.text = format.format(username)
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
drawable,null,null,null
|
||||
)
|
||||
}
|
||||
private fun setNotificationTypeTextView(context: Context, format: Int, drawable: Int): Pair<String, Drawable?> {
|
||||
return Pair(context.getString(format), context.getDrawable(drawable))
|
||||
}
|
||||
|
||||
|
||||
inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
|
||||
val notificationType: TextView = mView.notification_type
|
||||
val postDescription: TextView = mView.notification_post_description
|
||||
val avatar: ImageView = mView.notification_avatar
|
||||
val photoThumbnail: ImageView = mView.notification_photo_thumbnail
|
||||
}
|
||||
|
||||
override fun getPreloadItems(position: Int): MutableList<Notification> {
|
||||
val notification = getItem(position) ?: return mutableListOf()
|
||||
return mutableListOf(notification)
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: Notification): RequestBuilder<*>? {
|
||||
return profilePicRequest.load(item.account.avatar_static)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.h.pixeldroid.PostActivity
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
import kotlinx.android.synthetic.main.fragment_notifications.view.*
|
||||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that can display a [Notification]
|
||||
*/
|
||||
class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
private val mOnClickListener: View.OnClickListener
|
||||
|
||||
init {
|
||||
mOnClickListener = View.OnClickListener { v ->
|
||||
val notification = v.tag as Notification
|
||||
openActivity(notification)
|
||||
}
|
||||
}
|
||||
private fun openActivity(notification: Notification){
|
||||
val intent: Intent
|
||||
when (notification.type){
|
||||
Notification.NotificationType.mention, Notification.NotificationType.favourite-> {
|
||||
intent = Intent(context, PostActivity::class.java)
|
||||
intent.putExtra(POST_TAG, notification.status)
|
||||
}
|
||||
Notification.NotificationType.reblog-> {
|
||||
Toast.makeText(context,"Can't see shares yet, sorry!",Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
Notification.NotificationType.follow -> {
|
||||
val url = notification.status?.url ?: notification.account.url
|
||||
intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
}
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun addNotifications(newNotifications: List<Notification>){
|
||||
val oldSize = feedContent.size
|
||||
feedContent.addAll(newNotifications)
|
||||
notifyItemRangeInserted(oldSize, newNotifications.size)
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.fragment_notifications, parent, false)
|
||||
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val notification = feedContent[position]
|
||||
Glide.with(holder.mView).load(notification.account.avatar_static).apply(RequestOptions().circleCrop())
|
||||
.placeholder(R.drawable.ic_default_user).into(holder.avatar)
|
||||
|
||||
val previewUrl = notification.status?.media_attachments?.getOrNull(0)?.preview_url
|
||||
if(!previewUrl.isNullOrBlank()){
|
||||
Glide.with(holder.mView).load(previewUrl)
|
||||
.placeholder(R.drawable.ic_picture_fallback).into(holder.photoThumbnail)
|
||||
} else{
|
||||
holder.photoThumbnail.visibility = View.GONE
|
||||
}
|
||||
|
||||
setNotificationType(notification.type, notification.account.username, holder.notificationType)
|
||||
|
||||
holder.postDescription.text = notification.status?.content ?: ""
|
||||
|
||||
|
||||
with(holder.mView) {
|
||||
tag = notification
|
||||
setOnClickListener(mOnClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotificationType(type: Notification.NotificationType, username: String,
|
||||
textView: TextView){
|
||||
val context = textView.context
|
||||
val (format: String, drawable: Drawable?) = when(type) {
|
||||
Notification.NotificationType.follow -> {
|
||||
setNotificationTypeTextView(context, R.string.followed_notification, R.drawable.ic_follow)
|
||||
}
|
||||
Notification.NotificationType.mention -> {
|
||||
setNotificationTypeTextView(context, R.string.mention_notification, R.drawable.ic_apenstaart)
|
||||
}
|
||||
|
||||
Notification.NotificationType.reblog -> {
|
||||
setNotificationTypeTextView(context, R.string.shared_notification, R.drawable.ic_share)
|
||||
}
|
||||
|
||||
Notification.NotificationType.favourite -> {
|
||||
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_heart)
|
||||
}
|
||||
}
|
||||
textView.text = format.format(username)
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
drawable,null,null,null
|
||||
)
|
||||
}
|
||||
private fun setNotificationTypeTextView(context: Context, format: Int, drawable: Int): Pair<String, Drawable?> {
|
||||
return Pair(context.getString(format), context.getDrawable(drawable))
|
||||
}
|
||||
|
||||
|
||||
inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
|
||||
val notificationType: TextView = mView.notification_type
|
||||
val postDescription: TextView = mView.notification_post_description
|
||||
val avatar: ImageView = mView.notification_avatar
|
||||
val photoThumbnail: ImageView = mView.notification_photo_thumbnail
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.h.pixeldroid.objects
|
||||
|
||||
abstract class FeedContent {
|
||||
abstract val id: String
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
}
|
|
@ -4,15 +4,15 @@ package com.h.pixeldroid.objects
|
|||
Represents a notification of an event relevant to the user.
|
||||
https://docs.joinmastodon.org/entities/notification/
|
||||
*/
|
||||
data class Notification (
|
||||
data class Notification(
|
||||
//Required attributes
|
||||
val id: String,
|
||||
override val id: String,
|
||||
val type: NotificationType,
|
||||
val created_at: String, //ISO 8601 Datetime
|
||||
val account: Account,
|
||||
//Optional attributes
|
||||
val status: Status? = null
|
||||
) {
|
||||
): FeedContent() {
|
||||
enum class NotificationType {
|
||||
follow, mention, reblog, favourite
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.widget.TextView
|
|||
import androidx.fragment.app.Fragment
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import kotlinx.android.synthetic.main.post_fragment.view.*
|
||||
import java.io.Serializable
|
||||
|
||||
/*
|
||||
|
@ -14,7 +15,7 @@ https://docs.joinmastodon.org/entities/status/
|
|||
*/
|
||||
data class Status(
|
||||
//Base attributes
|
||||
val id: String,
|
||||
override val id: String,
|
||||
val uri: String,
|
||||
val created_at: String, //ISO 8601 Datetime (maybe can use a date type)
|
||||
val account: Account,
|
||||
|
@ -47,7 +48,7 @@ data class Status(
|
|||
val muted: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
val pinned: Boolean
|
||||
) : Serializable
|
||||
) : Serializable, FeedContent()
|
||||
{
|
||||
|
||||
companion object {
|
||||
|
@ -67,15 +68,12 @@ data class Status(
|
|||
}
|
||||
|
||||
fun getUsername() : CharSequence {
|
||||
var name = account?.username
|
||||
var name = account?.display_name
|
||||
if (name.isNullOrEmpty()) {
|
||||
name = account?.display_name
|
||||
name = account?.username
|
||||
}
|
||||
return name!!
|
||||
}
|
||||
fun getUsernameDescription() : CharSequence {
|
||||
return account?.display_name ?: ""
|
||||
}
|
||||
|
||||
fun getNLikes() : CharSequence {
|
||||
val nLikes : Int = favourites_count ?: 0
|
||||
|
@ -87,38 +85,22 @@ data class Status(
|
|||
return "$nShares Shares"
|
||||
}
|
||||
|
||||
fun setupPost(fragment: Fragment, rootView : View) {
|
||||
fun setupPost(rootView : View) {
|
||||
//Setup username as a button that opens the profile
|
||||
val username = rootView.findViewById<TextView>(R.id.username)
|
||||
username.text = this.getUsername()
|
||||
username.setTypeface(null, Typeface.BOLD)
|
||||
rootView.username.text = this.getUsername()
|
||||
rootView.username.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
val usernameDesc = rootView.findViewById<TextView>(R.id.usernameDesc)
|
||||
usernameDesc.text = this.getUsernameDescription()
|
||||
usernameDesc.setTypeface(null, Typeface.BOLD)
|
||||
rootView.usernameDesc.text = this.getUsername()
|
||||
rootView.usernameDesc.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
val description = rootView.findViewById<TextView>(R.id.description)
|
||||
description.text = this.getDescription()
|
||||
rootView.description.text = this.getDescription()
|
||||
|
||||
val nlikes = rootView.findViewById<TextView>(R.id.nlikes)
|
||||
nlikes.text = this.getNLikes()
|
||||
nlikes.setTypeface(null, Typeface.BOLD)
|
||||
rootView.nlikes.text = this.getNLikes()
|
||||
rootView.nlikes.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
val nshares = rootView.findViewById<TextView>(R.id.nshares)
|
||||
nshares.text = this.getNShares()
|
||||
nshares.setTypeface(null, Typeface.BOLD)
|
||||
rootView.nshares.text = this.getNShares()
|
||||
rootView.nshares.setTypeface(null, Typeface.BOLD)
|
||||
|
||||
//Setup post and profile images
|
||||
ImageConverter.setImageViewFromURL(
|
||||
fragment,
|
||||
getPostUrl(),
|
||||
rootView.findViewById(R.id.postPicture)
|
||||
)
|
||||
ImageConverter.setImageViewFromURL(
|
||||
fragment,
|
||||
getProfilePicUrl(),
|
||||
rootView.findViewById(R.id.profilePic)
|
||||
)
|
||||
}
|
||||
enum class Visibility : Serializable {
|
||||
public, unlisted, private, direct
|
||||
|
|
|
@ -52,6 +52,7 @@ tools:context=".fragments.PostFragment">
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/postPicture"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
@ -73,7 +74,7 @@ tools:context=".fragments.PostFragment">
|
|||
android:id="@+id/nlikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="30dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_weight="50"
|
||||
|
@ -84,10 +85,10 @@ tools:context=".fragments.PostFragment">
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginRight="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_weight="50"
|
||||
android:gravity="right"
|
||||
android:gravity="end"
|
||||
tools:text="TextView" />
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -95,7 +96,7 @@ tools:context=".fragments.PostFragment">
|
|||
android:id="@+id/usernameDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
tools:text="TextView" />
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class PostUnitTest {
|
|||
fun getDescriptionReturnsACorrectDesc() = Assert.assertEquals(status.content, status.getDescription())
|
||||
|
||||
@Test
|
||||
fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account.username, status.getUsername())
|
||||
fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account.display_name, status.getUsername())
|
||||
|
||||
@Test
|
||||
fun getUsernameReturnsOtherNameIfUsernameIsNull() {
|
||||
|
|
Loading…
Reference in New Issue