adding media gallery timeline

This commit is contained in:
Mariotaku Lee 2017-10-21 01:39:48 +08:00
parent c5713f5be0
commit 315dcd9002
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
14 changed files with 313 additions and 52 deletions

View File

@ -2,11 +2,11 @@
buildscript {
repositories {
jcenter()
google()
maven { url 'https://plugins.gradle.org/m2/' }
maven { url 'https://maven.google.com' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-rc1'
classpath 'com.android.tools.build:gradle:3.0.0-rc2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@ -26,7 +26,7 @@ allprojects {
repositories {
mavenLocal()
jcenter()
maven { url 'https://maven.google.com' }
google()
maven { url 'https://jitpack.io' }
}
@ -82,6 +82,7 @@ subprojects {
StethoBeanShellREPL : '0.3',
ArchLifecycleExtensions: '1.0.0-beta2',
ArchPaging : '1.0.0-alpha3',
ConstraintLayout : '1.0.2',
]
}

View File

@ -209,6 +209,7 @@ dependencies {
implementation "com.android.support:customtabs:${libVersions['SupportLib']}"
implementation "com.android.support:design:${libVersions['SupportLib']}"
implementation "com.android.support:exifinterface:${libVersions['SupportLib']}"
implementation "com.android.support.constraint:constraint-layout:${libVersions['ConstraintLayout']}"
implementation "com.twitter:twitter-text:${libVersions['TwitterText']}"
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.6.0'
implementation 'com.squareup:otto:1.3.8'

View File

@ -36,7 +36,7 @@ class AccountSelectorAdapter(
private val inflater: LayoutInflater,
preferences: SharedPreferences,
val requestManager: RequestManager
) : RecyclerPagerAdapter() {
) : RecyclerPagerAdapter<RecyclerPagerAdapter.ViewHolder>() {
internal var profileImageStyle: Int = preferences[profileImageStyleKey]

View File

@ -62,7 +62,7 @@ import java.util.*
class ParcelableStatusesAdapter(
context: Context,
requestManager: RequestManager,
@TimelineStyle val timelineStyle: Int
@TimelineStyle private val timelineStyle: Int
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context, requestManager),
IStatusesAdapter, IItemCountsAdapter {
@ -457,19 +457,26 @@ class ParcelableStatusesAdapter(
parent: ViewGroup, @TimelineStyle timelineStyle: Int): IStatusViewHolder {
when (timelineStyle) {
TimelineStyle.STAGGERED -> {
val view = inflater.inflate(R.layout.adapter_item_media_status, parent, false)
val view = inflater.inflate(MediaStatusViewHolder.layoutResource, parent, false)
val holder = MediaStatusViewHolder(adapter, view)
holder.setOnClickListeners()
holder.setupViewOptions()
return holder
}
TimelineStyle.PLAIN, TimelineStyle.GALLERY -> {
TimelineStyle.PLAIN -> {
val view = inflater.inflate(StatusViewHolder.layoutResource, parent, false)
val holder = StatusViewHolder(adapter, view)
holder.setOnClickListeners()
holder.setupViewOptions()
return holder
}
TimelineStyle.GALLERY -> {
val view = inflater.inflate(LargeMediaStatusViewHolder.layoutResource, parent, false)
val holder = LargeMediaStatusViewHolder(adapter, view)
holder.setOnClickListeners()
holder.setupViewOptions()
return holder
}
else -> throw AssertionError()
}
}

View File

@ -5,11 +5,8 @@ import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
/**
* Created by mariotaku on 2016/12/9.
*/
abstract class RecyclerPagerAdapter : PagerAdapter() {
private val viewHolders: SparseArray<ViewHolder> = SparseArray()
abstract class RecyclerPagerAdapter<VH : RecyclerPagerAdapter.ViewHolder> : PagerAdapter() {
private val viewHolders: SparseArray<VH> = SparseArray()
final override fun instantiateItem(container: ViewGroup, position: Int): Any {
val itemViewType = getItemViewType(position)
@ -44,9 +41,9 @@ abstract class RecyclerPagerAdapter : PagerAdapter() {
return (obj as ViewHolder).itemView == view
}
abstract fun onCreateViewHolder(container: ViewGroup, position: Int, itemViewType: Int): ViewHolder
abstract fun onCreateViewHolder(container: ViewGroup, position: Int, itemViewType: Int): VH
abstract fun onBindViewHolder(holder: ViewHolder, position: Int, itemViewType: Int)
abstract fun onBindViewHolder(holder: VH, position: Int, itemViewType: Int)
open fun getItemViewType(position: Int): Int = 0

View File

@ -21,16 +21,14 @@ package org.mariotaku.twidere.data.source
import android.arch.paging.TiledDataSource
import android.content.ContentResolver
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.os.Looper
import org.mariotaku.ktextension.weak
import org.mariotaku.twidere.extension.queryAll
import org.mariotaku.twidere.extension.queryCount
/**
* Created by mariotaku on 2017/10/13.
*/
class CursorObjectTiledDataSource<T>(
private val resolver: ContentResolver,
val uri: Uri,
@ -41,6 +39,19 @@ class CursorObjectTiledDataSource<T>(
val cls: Class<T>
) : TiledDataSource<T>() {
init {
val weakThis = weak()
val observer = object : ContentObserver(MainHandler) {
override fun onChange(selfChange: Boolean) {
weakThis.get()?.invalidate()
}
}
addInvalidatedCallback cb@ {
resolver.unregisterContentObserver(observer)
}
resolver.registerContentObserver(uri, false, observer)
}
override fun countItems() = resolver.queryCount(uri, selection, selectionArgs)
override fun loadRange(startPosition: Int, count: Int): List<T> {

View File

@ -126,9 +126,7 @@ class AddStatusFilterDialogFragment : BaseDialogFragment() {
private val filterItemsInfo: Array<FilterItemInfo>
get() {
val args = arguments
if (args == null || !args.containsKey(EXTRA_STATUS)) return emptyArray()
val status = args.getParcelable<ParcelableStatus>(EXTRA_STATUS) ?: return emptyArray()
val status = arguments.getParcelable<ParcelableStatus>(EXTRA_STATUS) ?: return emptyArray()
val list = ArrayList<FilterItemInfo>()
if (status.is_retweet && status.retweeted_by_user_key != null) {
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_USER,
@ -143,12 +141,8 @@ class AddStatusFilterDialogFragment : BaseDialogFragment() {
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_USER, UserItem(status.user_key,
status.user_name, status.user_screen_name)))
val mentions = status.mentions
if (mentions != null) {
for (mention in mentions) {
if (mention.key != status.user_key) {
list.add(FilterItemInfo(FilterItemInfo.FILTER_TYPE_USER, mention))
}
}
mentions?.filter { it.key != status.user_key }?.mapTo(list) {
FilterItemInfo(FilterItemInfo.FILTER_TYPE_USER, it)
}
val hashtags = HashSet<String>()
hashtags.addAll(extractor.extractHashtags(status.text_plain))

View File

@ -56,12 +56,9 @@ import org.mariotaku.twidere.adapter.iface.IContentAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.annotation.TimelineStyle
import org.mariotaku.twidere.constant.*
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
import org.mariotaku.twidere.constant.displaySensitiveContentsKey
import org.mariotaku.twidere.constant.favoriteConfirmationKey
import org.mariotaku.twidere.constant.loadItemLimitKey
import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.data.fetcher.StatusesFetcher
import org.mariotaku.twidere.data.source.CursorObjectLivePagedListProvider
import org.mariotaku.twidere.data.status.StatusesLivePagedListProvider
@ -138,6 +135,7 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
registerForContextMenu(recyclerView)
adapter.statusClickListener = StatusClickHandler()
statuses = createLiveData()
statuses.observe(this, Observer { onDataLoaded(it) })
@ -166,12 +164,9 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
override fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView,
layoutManager: LayoutManager): RecyclerView.ItemDecoration? {
return when (timelineStyle) {
TimelineStyle.PLAIN -> {
createStatusesListItemDecoration(context, recyclerView, adapter)
}
else -> {
super.onCreateItemDecoration(context, recyclerView, layoutManager)
}
TimelineStyle.PLAIN -> createStatusesListItemDecoration(context, recyclerView, adapter)
TimelineStyle.GALLERY -> createStatusesListGalleryDecoration(context, recyclerView)
else -> super.onCreateItemDecoration(context, recyclerView, layoutManager)
}
}
@ -287,12 +282,10 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
protected open fun onDataLoaded(data: PagedList<ParcelableStatus>?) {
val firstVisiblePosition = layoutManager.firstVisibleItemPosition
adapter.statuses = data
adapter.timelineFilter = timelineFilter
when {
// data is ExceptionResponseList -> {
// showEmpty(R.drawable.ic_info_error_generic, data.exception.toString())
// }
data == null || data.isEmpty() -> {
showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh))
}
@ -300,6 +293,11 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
showContent()
}
}
if (firstVisiblePosition == 0 && !preferences[readFromBottomKey]) {
recyclerView.post {
recyclerView.smoothScrollToPosition(0)
}
}
}
protected abstract fun getStatuses(param: ContentRefreshParam): Boolean
@ -366,9 +364,10 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
extraSelection.second?.addAllTo(expressionArgs)
}
val provider = CursorObjectLivePagedListProvider(context.contentResolver, contentUri,
statusColumnsLite, Expression.and(*expressions.toTypedArray()).sql,
Statuses.COLUMNS, Expression.and(*expressions.toTypedArray()).sql,
expressionArgs.toTypedArray(), Statuses.DEFAULT_SORT_ORDER, ParcelableStatus::class.java)
return provider.create(null, 20)
return provider.create(null, PagedList.Config.Builder()
.setPageSize(50).setEnablePlaceholders(false).build())
}
private fun getFullStatus(position: Int): ParcelableStatus? {
@ -441,6 +440,12 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
IntentUtils.openStatus(activity, status, null)
}
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
val status = adapter.getStatus(position)
System.identityHashCode(status)
return false
}
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {
val status = getFullStatus(position) ?: return
handleActionClick(this@AbsTimelineFragment, id, status,
@ -476,10 +481,6 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
IntentUtils.openStatus(activity, status.account_key, quotedId)
}
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
return super.onStatusLongClick(holder, position)
}
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)
val intent = IntentUtils.userProfile(status.account_key, status.user_key,
@ -683,6 +684,12 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
return itemDecoration
}
fun createStatusesListGalleryDecoration(context: Context, recyclerView: RecyclerView): RecyclerView.ItemDecoration {
val itemDecoration = ExtendedDividerItemDecoration(context, (recyclerView.layoutManager as LinearLayoutManager).orientation)
itemDecoration.setDecorationEndOffset(1)
return itemDecoration
}
fun selectAccountIntent(context: Context, status: ParcelableStatus, itemId: Long,
sameHostOnly: Boolean = true): Intent {
val intent = Intent(context, AccountSelectorActivity::class.java)

View File

@ -24,6 +24,7 @@ import android.os.Bundle
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.annotation.TimelineStyle
import org.mariotaku.twidere.constant.IntentConstants
import org.mariotaku.twidere.data.fetcher.StatusesFetcher
import org.mariotaku.twidere.data.fetcher.UserMediaTimelineFetcher
@ -39,6 +40,8 @@ class UserMediaTimelineFragment : AbsTimelineFragment() {
override val contentUri: Uri = Statuses.UserTimeline.CONTENT_URI
override val timelineStyle: Int = TimelineStyle.GALLERY
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
linkHandlerTitle = getString(R.string.title_media_timeline)

View File

@ -0,0 +1,143 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.view.holder
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.bumptech.glide.RequestManager
import kotlinx.android.synthetic.main.adapter_item_large_media_status.view.*
import kotlinx.android.synthetic.main.adapter_item_large_media_status_preview_item.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.RecyclerPagerAdapter
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.extension.loadProfileImage
import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable
import org.mariotaku.twidere.model.ParcelableMedia
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.view.ProfileImageView
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
class LargeMediaStatusViewHolder(private val adapter: IStatusesAdapter, itemView: View) :
RecyclerView.ViewHolder(itemView), IStatusViewHolder, View.OnClickListener, View.OnLongClickListener {
override val profileImageView: ProfileImageView = itemView.profileImage
override val profileTypeView: ImageView? = null
private val mediaPreviewAdapter: ImagePagerAdapter
private val mediaPreviewPager = itemView.mediaPreviewPager
private val nameView = itemView.nameView
private var listener: IStatusViewHolder.StatusClickListener? = null
init {
mediaPreviewAdapter = ImagePagerAdapter(adapter.requestManager)
mediaPreviewPager.adapter = mediaPreviewAdapter
}
override fun display(status: ParcelableStatus, displayInReplyTo: Boolean,
displayPinned: Boolean) {
val context = itemView.context
adapter.requestManager.loadProfileImage(context, status,
adapter.profileImageStyle, profileImageView.cornerRadius,
profileImageView.cornerRadiusRatio).into(profileImageView)
nameView.name = status.user_name
nameView.screenName = "@${status.user_screen_name}"
nameView.updateText(adapter.bidiFormatter)
mediaPreviewAdapter.media = status.media
}
override fun onClick(v: View) {
when (v.id) {
R.id.itemContent -> {
listener?.onStatusClick(this, layoutPosition)
}
}
}
override fun onLongClick(v: View): Boolean {
return false
}
override fun onMediaClick(view: View, current: ParcelableMedia, accountKey: UserKey?, id: Long) {
}
override fun setStatusClickListener(listener: IStatusViewHolder.StatusClickListener?) {
this.listener = listener
itemView.itemContent.setOnClickListener(this)
}
override fun setTextSize(textSize: Float) {
}
override fun playLikeAnimation(listener: LikeAnimationDrawable.OnLikedListener) {
}
fun setOnClickListeners() {
setStatusClickListener(adapter.statusClickListener)
}
fun setupViewOptions() {
setTextSize(adapter.textSize)
nameView.nameFirst = adapter.nameFirst
}
private class ImagePagerAdapter(val requestManager: RequestManager) : RecyclerPagerAdapter<LargeMediaItemHolder>() {
var media: Array<ParcelableMedia>? = null
set(value) {
field = value
notifyDataSetChanged()
}
override fun getCount() = media?.size ?: 0
override fun onCreateViewHolder(container: ViewGroup, position: Int, itemViewType: Int): LargeMediaItemHolder {
return LargeMediaItemHolder(this, LayoutInflater.from(container.context)
.inflate(LargeMediaItemHolder.layoutResource, container, false))
}
override fun onBindViewHolder(holder: LargeMediaItemHolder, position: Int, itemViewType: Int) {
holder.display(media!![position])
}
}
private class LargeMediaItemHolder(val adapter: ImagePagerAdapter, itemView: View) : RecyclerPagerAdapter.ViewHolder(itemView) {
private val mediaPreview = itemView.mediaPreview
fun display(media: ParcelableMedia) {
adapter.requestManager.load(media.preview_url).into(mediaPreview)
}
companion object {
val layoutResource = R.layout.adapter_item_large_media_status_preview_item
}
}
companion object {
const val layoutResource = R.layout.adapter_item_large_media_status
}
}

View File

@ -38,20 +38,15 @@ import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
class MediaStatusViewHolder(private val adapter: IStatusesAdapter, itemView: View) : RecyclerView.ViewHolder(itemView), IStatusViewHolder, View.OnClickListener, View.OnLongClickListener {
override val profileImageView: ProfileImageView = itemView.mediaProfileImage
override val profileTypeView: ImageView? = null
private val mediaImageContainer = itemView.mediaImageContainer
private val mediaImageView = itemView.mediaImage
private val mediaTextView = itemView.mediaText
private val aspectRatioSource = SimpleAspectRatioSource()
private var listener: IStatusViewHolder.StatusClickListener? = null
override val profileTypeView: ImageView?
get() = null
init {
mediaImageContainer.setAspectRatioSource(aspectRatioSource)
}
@ -140,4 +135,8 @@ class MediaStatusViewHolder(private val adapter: IStatusesAdapter, itemView: Vie
}
}
companion object {
const val layoutResource = R.layout.adapter_item_media_status
}
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemContent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.mariotaku.twidere.view.ProfileImageView
android:id="@+id/profileImage"
android:layout_width="@dimen/icon_size_large_media_status_profile_image"
android:layout_height="@dimen/icon_size_large_media_status_profile_image"
android:layout_marginLeft="@dimen/element_spacing_large"
android:layout_marginStart="@dimen/element_spacing_large"
android:layout_marginTop="@dimen/element_spacing_large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher"/>
<org.mariotaku.twidere.view.NameView
android:id="@+id/nameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/element_spacing_normal"
android:layout_marginStart="@dimen/element_spacing_normal"
app:layout_constraintBottom_toBottomOf="@+id/profileImage"
app:layout_constraintStart_toEndOf="@+id/profileImage"
app:layout_constraintTop_toTopOf="@+id/profileImage"
app:nv_primaryTextColor="?android:textColorPrimary"
app:nv_primaryTextStyle="bold"
app:nv_secondaryTextColor="?android:textColorSecondary"
tools:text="Username"/>
<org.mariotaku.twidere.view.ExtendedViewPager
android:id="@+id/mediaPreviewPager"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="@dimen/element_spacing_large"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profileImage"
tools:background="@drawable/nyan_stars_background"/>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/mediaPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
</FrameLayout>

View File

@ -80,6 +80,7 @@
<dimen name="icon_size_profile_type_detail">22dp</dimen>
<dimen name="icon_size_profile_type_user_profile">26dp</dimen>
<dimen name="icon_size_status_profile_image">48dp</dimen>
<dimen name="icon_size_large_media_status_profile_image">32dp</dimen>
<dimen name="icon_size_user_profile">84dp</dimen>
<dimen name="icon_size_conversation_info">64dp</dimen>