Infinite loading, make glide aware of the recyclerview

This commit is contained in:
Matthieu 2020-04-02 19:57:07 +02:00
parent ff4476247c
commit ba8980652c
12 changed files with 449 additions and 298 deletions

View File

@ -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'
}

View File

@ -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>>

View File

@ -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
}

View File

@ -9,21 +9,32 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
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 com.h.pixeldroid.objects.Notification
import kotlinx.android.synthetic.main.fragment_feed.*
import kotlinx.android.synthetic.main.fragment_feed.view.*
import kotlinx.android.synthetic.main.fragment_feed.view.progressBar
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
var content : List<T> = ArrayList()
open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment() {
lateinit var content: LiveData<PagedList<T>>
protected var accessToken: String? = null
protected lateinit var pixelfedAPI: PixelfedAPI
@ -33,34 +44,14 @@ 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 +60,6 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -81,18 +71,53 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
accessToken = preferences.getString("accessToken", "")
}
internal fun enqueueCall(call: Call<List<T>>, callback: ItemKeyedDataSource.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())
}
})
}
}
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
}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
}
}
), PreloadModelProvider<T> {
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()
abstract class FeedDataSource: ItemKeyedDataSource<String, FeedContent>() {
override fun getKey(item: FeedContent): String {
return item.id
}
}
}
abstract class FeedDataSourceFactory: DataSource.Factory<String, FeedContent>()

View File

@ -1,22 +1,63 @@
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.Observer
import androidx.paging.DataSource
import androidx.paging.ItemKeyedDataSource
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.*
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)
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
val factory = HomeDataSourceFactory()
content = LivePagedListBuilder(factory, config).build()
picRequest = Glide.with(this)
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
adapter = HomeRecyclerViewAdapter()
list.adapter = adapter
content.observe(viewLifecycleOwner,
Observer { c -> adapter.submitList(c); })
val sizeProvider: ListPreloader.PreloadSizeProvider<Status> = ViewPreloadSizeProvider()
val preloader: RecyclerViewPreloader<Status> = RecyclerViewPreloader(
Glide.with(this), adapter, sizeProvider, 4
)
list.addOnScrollListener(preloader)
return view
}
@ -25,10 +66,106 @@ class HomeFragment : FeedFragment<Status, HomeRecyclerViewAdapter.ViewHolder>()
swipeRefreshLayout.setOnRefreshListener {
val call = pixelfedAPI.timelineHome("Bearer $accessToken")
doRequest(call)
val call = pixelfedAPI.timelineHome("Bearer $accessToken", limit = "20")
//TODO
}
val call = pixelfedAPI.timelineHome("Bearer $accessToken")
doRequest(call)
}
/**
* [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())
}
}
inner class HomeDataSource: ItemKeyedDataSource<String, Status>() {
override fun getKey(item: Status): String {
return item.id
}
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<Status>
) {
val call = pixelfedAPI
.timelineHome("Bearer $accessToken", limit="${params.requestedLoadSize}")
enqueueCall(call, callback)
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Status>) {
val call = pixelfedAPI
.timelineHome("Bearer $accessToken", max_id=params.key,
limit="${params.requestedLoadSize}")
enqueueCall(call, callback)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Status>) {
//do nothing here, it is expected to pull to refresh to load newer notifications
}
}
inner class HomeDataSourceFactory: DataSource.Factory<String, Status>() {
override fun create(): DataSource<String, Status> {
return HomeDataSource()
}
}
}

View File

@ -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)
}
}

View File

@ -1,30 +1,74 @@
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.util.Log
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.Observer
import androidx.paging.DataSource
import androidx.paging.ItemKeyedDataSource
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
import retrofit2.Callback
import retrofit2.Response
/**
* 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)
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
val factory = NotificationsDataSourceFactory()
content = LivePagedListBuilder(factory, config).build()
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); })
val sizeProvider: ListPreloader.PreloadSizeProvider<Notification> = ViewPreloadSizeProvider()
val preloader: RecyclerViewPreloader<Notification> = RecyclerViewPreloader(
Glide.with(this), adapter, sizeProvider, 4
)
list.addOnScrollListener(preloader)
return view
}
@ -34,11 +78,150 @@ class NotificationsFragment : FeedFragment<Notification, NotificationsRecyclerVi
swipeRefreshLayout.setOnRefreshListener {
val call = pixelfedAPI.notifications("Bearer $accessToken", min_id="1")
doRequest(call)
//TODO
}
val call = pixelfedAPI.notifications("Bearer $accessToken", min_id="1")
doRequest(call)
}
/**
* [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)
}
}
inner class NotificationsDataSource: ItemKeyedDataSource<String, Notification>() {
override fun getKey(item: Notification): String {
return item.id
}
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<Notification>
) {
val call = pixelfedAPI
.notifications("Bearer $accessToken", min_id="1", limit="${params.requestedLoadSize}")
enqueueCall(call, callback)
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Notification>) {
val call = pixelfedAPI
.notifications("Bearer $accessToken", max_id=params.key, limit="${params.requestedLoadSize}")
enqueueCall(call, callback)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Notification>) {
//do nothing here, it is expected to pull to refresh to load newer notifications
}
}
inner class NotificationsDataSourceFactory: DataSource.Factory<String, Notification>() {
override fun create(): DataSource<String, Notification> {
return NotificationsDataSource()
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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" />