326 lines
12 KiB
Kotlin
326 lines
12 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2015 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.fragment
|
|
|
|
import android.content.Context
|
|
import android.graphics.Rect
|
|
import android.os.Bundle
|
|
import android.support.v4.widget.SwipeRefreshLayout
|
|
import android.support.v7.widget.RecyclerView
|
|
import android.support.v7.widget.RecyclerView.ItemDecoration
|
|
import android.view.*
|
|
import kotlinx.android.synthetic.main.fragment_content_recyclerview.*
|
|
import kotlinx.android.synthetic.main.layout_content_fragment_common.*
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.activity.iface.IControlBarActivity
|
|
import org.mariotaku.twidere.activity.iface.IControlBarActivity.ControlBarShowHideHelper
|
|
import org.mariotaku.twidere.adapter.LoadMoreSupportAdapter
|
|
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
|
|
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition
|
|
import org.mariotaku.twidere.fragment.iface.RefreshScrollTopInterface
|
|
import org.mariotaku.twidere.util.*
|
|
import org.mariotaku.twidere.view.ExtendedSwipeRefreshLayout
|
|
import org.mariotaku.twidere.view.HeaderDrawerLayout
|
|
import org.mariotaku.twidere.view.iface.IExtendedView
|
|
|
|
/**
|
|
* Created by mariotaku on 15/10/26.
|
|
*/
|
|
abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<RecyclerView.ViewHolder>,
|
|
L : RecyclerView.LayoutManager> : BaseFragment(), SwipeRefreshLayout.OnRefreshListener,
|
|
HeaderDrawerLayout.DrawerCallback, RefreshScrollTopInterface, IControlBarActivity.ControlBarOffsetListener,
|
|
ContentScrollHandler.ContentListSupport<A>, ControlBarShowHideHelper.ControlBarAnimationListener {
|
|
|
|
lateinit var layoutManager: L
|
|
protected set
|
|
override lateinit var adapter: A
|
|
protected set
|
|
var itemDecoration: ItemDecoration? = null
|
|
private set
|
|
|
|
// Callbacks and listeners
|
|
private lateinit var drawerCallback: SimpleDrawerCallback
|
|
lateinit var scrollListener: RecyclerViewScrollHandler<A>
|
|
// Data fields
|
|
private val systemWindowsInsets = Rect()
|
|
|
|
private val refreshCompleteListener: RefreshCompleteListener?
|
|
get() = parentFragment as? RefreshCompleteListener
|
|
|
|
override fun canScroll(dy: Float): Boolean {
|
|
return drawerCallback.canScroll(dy)
|
|
}
|
|
|
|
override fun cancelTouch() {
|
|
drawerCallback.cancelTouch()
|
|
}
|
|
|
|
override fun fling(velocity: Float) {
|
|
drawerCallback.fling(velocity)
|
|
}
|
|
|
|
override fun isScrollContent(x: Float, y: Float): Boolean {
|
|
return drawerCallback.isScrollContent(x, y)
|
|
}
|
|
|
|
override fun onControlBarOffsetChanged(activity: IControlBarActivity, offset: Float) {
|
|
updateRefreshProgressOffset()
|
|
}
|
|
|
|
override final fun onRefresh() {
|
|
if (!triggerRefresh()) {
|
|
refreshing = false
|
|
}
|
|
}
|
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
super.setUserVisibleHint(isVisibleToUser)
|
|
updateRefreshProgressOffset()
|
|
}
|
|
|
|
override fun scrollBy(dy: Float) {
|
|
drawerCallback.scrollBy(dy)
|
|
}
|
|
|
|
override fun scrollToStart(): Boolean {
|
|
scrollToPositionWithOffset(0, 0)
|
|
recyclerView.stopScroll()
|
|
setControlVisible(true)
|
|
return true
|
|
}
|
|
|
|
protected abstract fun scrollToPositionWithOffset(position: Int, offset: Int)
|
|
|
|
override fun onControlBarVisibleAnimationFinish(visible: Boolean) {
|
|
updateRefreshProgressOffset()
|
|
}
|
|
|
|
override fun setControlVisible(visible: Boolean) {
|
|
val activity = activity
|
|
if (activity is IControlBarActivity) {
|
|
//TODO hide only if top > actionBar.height
|
|
val manager = layoutManager
|
|
if (manager.childCount == 0) return
|
|
val firstView = manager.getChildAt(0)
|
|
if (manager.getPosition(firstView) != 0) {
|
|
activity.setControlBarVisibleAnimate(visible, this)
|
|
return
|
|
}
|
|
val top = firstView.top
|
|
activity.setControlBarVisibleAnimate(visible || top > 0, this)
|
|
}
|
|
}
|
|
|
|
override fun shouldLayoutHeaderBottom(): Boolean {
|
|
return drawerCallback.shouldLayoutHeaderBottom()
|
|
}
|
|
|
|
override fun topChanged(offset: Int) {
|
|
drawerCallback.topChanged(offset)
|
|
}
|
|
|
|
override var refreshing: Boolean
|
|
get () = swipeLayout.isRefreshing
|
|
set(value) {
|
|
val currentRefreshing = swipeLayout.isRefreshing
|
|
if (!currentRefreshing) {
|
|
updateRefreshProgressOffset()
|
|
}
|
|
if (!value) {
|
|
refreshCompleteListener?.onRefreshComplete(this)
|
|
}
|
|
if (value == currentRefreshing) return
|
|
val layoutRefreshing = value && adapter.loadMoreIndicatorPosition != ILoadMoreSupportAdapter.NONE
|
|
swipeLayout.isRefreshing = layoutRefreshing
|
|
}
|
|
|
|
var refreshEnabled: Boolean
|
|
get() = swipeLayout.isEnabled
|
|
set(value) {
|
|
swipeLayout.isEnabled = value
|
|
}
|
|
|
|
override fun onLoadMoreContents(@IndicatorPosition position: Long) {
|
|
setLoadMoreIndicatorPosition(position)
|
|
refreshEnabled = position == ILoadMoreSupportAdapter.NONE
|
|
}
|
|
|
|
override fun onAttach(context: Context?) {
|
|
super.onAttach(context)
|
|
if (context is IControlBarActivity) {
|
|
context.registerControlBarOffsetListener(this)
|
|
}
|
|
}
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
return inflater.inflate(R.layout.fragment_content_recyclerview, container, false)
|
|
}
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
super.onActivityCreated(savedInstanceState)
|
|
drawerCallback = SimpleDrawerCallback(recyclerView)
|
|
|
|
val backgroundColor = ThemeUtils.getColorBackground(context)
|
|
val colorRes = TwidereColorUtils.getContrastYIQ(backgroundColor,
|
|
R.color.bg_refresh_progress_color_light, R.color.bg_refresh_progress_color_dark)
|
|
swipeLayout.setOnRefreshListener(this)
|
|
swipeLayout.setProgressBackgroundColorSchemeResource(colorRes)
|
|
adapter = onCreateAdapter(context)
|
|
layoutManager = onCreateLayoutManager(context)
|
|
scrollListener = RecyclerViewScrollHandler(this, RecyclerViewScrollHandler.RecyclerViewCallback(recyclerView))
|
|
|
|
recyclerView.layoutManager = layoutManager
|
|
recyclerView.setHasFixedSize(true)
|
|
val swipeLayout = swipeLayout
|
|
if (swipeLayout is ExtendedSwipeRefreshLayout) {
|
|
swipeLayout.setTouchInterceptor(object : IExtendedView.TouchInterceptor {
|
|
override fun dispatchTouchEvent(view: View, event: MotionEvent): Boolean {
|
|
scrollListener.touchListener.onTouch(view, event)
|
|
return false
|
|
}
|
|
|
|
override fun onInterceptTouchEvent(view: View, event: MotionEvent): Boolean {
|
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
|
updateRefreshProgressOffset()
|
|
}
|
|
return false
|
|
}
|
|
|
|
override fun onTouchEvent(view: View, event: MotionEvent): Boolean {
|
|
return false
|
|
}
|
|
|
|
})
|
|
} else {
|
|
recyclerView.setOnTouchListener(scrollListener.touchListener)
|
|
}
|
|
setupRecyclerView(context, recyclerView)
|
|
recyclerView.adapter = adapter
|
|
|
|
scrollListener.touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
|
|
}
|
|
|
|
protected open fun setupRecyclerView(context: Context, recyclerView: RecyclerView) {
|
|
itemDecoration = onCreateItemDecoration(context, recyclerView, layoutManager)
|
|
if (itemDecoration != null) {
|
|
recyclerView.addItemDecoration(itemDecoration)
|
|
}
|
|
}
|
|
|
|
protected abstract fun onCreateLayoutManager(context: Context): L
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
recyclerView.addOnScrollListener(scrollListener)
|
|
}
|
|
|
|
override fun onStop() {
|
|
recyclerView.removeOnScrollListener(scrollListener)
|
|
super.onStop()
|
|
}
|
|
|
|
|
|
override fun onDetach() {
|
|
val activity = activity
|
|
if (activity is IControlBarActivity) {
|
|
activity.unregisterControlBarOffsetListener(this)
|
|
}
|
|
super.onDetach()
|
|
}
|
|
|
|
protected open val extraContentPadding: Rect
|
|
get() = Rect()
|
|
|
|
override fun fitSystemWindows(insets: Rect) {
|
|
val extraPadding = extraContentPadding
|
|
recyclerView.setPadding(insets.left + extraPadding.left, insets.top + extraPadding.top,
|
|
insets.right + extraPadding.right, insets.bottom + extraPadding.bottom)
|
|
errorContainer.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
|
progressContainer.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
|
systemWindowsInsets.set(insets)
|
|
updateRefreshProgressOffset()
|
|
}
|
|
|
|
open fun setLoadMoreIndicatorPosition(@IndicatorPosition position: Long) {
|
|
adapter.loadMoreIndicatorPosition = position
|
|
}
|
|
|
|
override fun triggerRefresh(): Boolean {
|
|
return false
|
|
}
|
|
|
|
protected abstract fun onCreateAdapter(context: Context): A
|
|
|
|
|
|
protected open fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView,
|
|
layoutManager: L): ItemDecoration? {
|
|
return null
|
|
}
|
|
|
|
protected fun showContent() {
|
|
errorContainer.visibility = View.GONE
|
|
progressContainer.visibility = View.GONE
|
|
recyclerView.visibility = View.VISIBLE
|
|
}
|
|
|
|
protected fun showProgress() {
|
|
errorContainer.visibility = View.GONE
|
|
progressContainer.visibility = View.VISIBLE
|
|
recyclerView.visibility = View.GONE
|
|
}
|
|
|
|
protected fun showError(icon: Int, text: CharSequence) {
|
|
errorContainer.visibility = View.VISIBLE
|
|
progressContainer.visibility = View.GONE
|
|
recyclerView.visibility = View.GONE
|
|
errorIcon.setImageResource(icon)
|
|
errorText.text = text
|
|
}
|
|
|
|
protected fun showEmpty(icon: Int, text: CharSequence) {
|
|
errorContainer.visibility = View.VISIBLE
|
|
progressContainer.visibility = View.GONE
|
|
recyclerView.visibility = View.VISIBLE
|
|
errorIcon.setImageResource(icon)
|
|
errorText.text = text
|
|
}
|
|
|
|
protected fun updateRefreshProgressOffset() {
|
|
val activity = activity
|
|
val insets = this.systemWindowsInsets
|
|
if (activity !is IControlBarActivity || insets.top == 0 || swipeLayout == null || refreshing) {
|
|
return
|
|
}
|
|
val progressCircleDiameter = swipeLayout.progressCircleDiameter
|
|
if (progressCircleDiameter == 0) return
|
|
val density = resources.displayMetrics.density
|
|
val controlBarOffsetPixels = Math.round(activity.controlBarHeight * (1 - activity.controlBarOffset))
|
|
val swipeStart = insets.top - controlBarOffsetPixels - progressCircleDiameter
|
|
// 64: SwipeRefreshLayout.DEFAULT_CIRCLE_TARGET
|
|
val swipeDistance = Math.round(64 * density)
|
|
swipeLayout.setProgressViewOffset(false, swipeStart, swipeStart + swipeDistance)
|
|
}
|
|
|
|
interface RefreshCompleteListener {
|
|
fun onRefreshComplete(fragment: AbsContentRecyclerViewFragment<*, *>)
|
|
}
|
|
}
|