From d683cae6ffe644a9f63eea6cf3b7e59d2bde617b Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 21 Dec 2017 02:16:34 -0500 Subject: Enhancement/fragment interface (#564) * Begin fragment interfaces and themable contracts * Prepare swiperefresh interface * Snapshot * Add compilable version * Revamp once more * Finalize layouts * Cleanup --- .../com/pitchedapps/frost/views/AccountItem.kt | 2 +- .../pitchedapps/frost/views/FrostContentView.kt | 140 ++++++++++++++++++++ .../pitchedapps/frost/views/FrostRecyclerView.kt | 103 +++++++++++++++ .../com/pitchedapps/frost/views/FrostWebView.kt | 144 +++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/views/Keywords.kt | 2 +- 5 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt (limited to 'app/src/main/kotlin/com/pitchedapps/frost/views') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt index 4b6c9e4e..2ab1d572 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt @@ -25,7 +25,7 @@ import com.pitchedapps.frost.utils.withRoundIcon class AccountItem(val cookie: CookieModel?) : KauIItem (R.layout.view_account, { ViewHolder(it) }, R.id.item_account) { - override fun bindView(viewHolder: ViewHolder, payloads: List?) { + override fun bindView(viewHolder: ViewHolder, payloads: MutableList) { super.bindView(viewHolder, payloads) with(viewHolder) { text.invisible() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt new file mode 100644 index 00000000..58449de3 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -0,0 +1,140 @@ +package com.pitchedapps.frost.views + +import android.content.Context +import android.os.Build +import android.support.v4.widget.SwipeRefreshLayout +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.ProgressBar +import ca.allanwang.kau.utils.* +import com.pitchedapps.frost.R +import com.pitchedapps.frost.contracts.FrostContentContainer +import com.pitchedapps.frost.contracts.FrostContentCore +import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.web.WEB_LOAD_DELAY +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject + +class FrostContentWeb @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 +) : FrostContentView(context, attrs, defStyleAttr, defStyleRes) { + + override val layoutRes: Int = R.layout.view_content_base_web + +} + +class FrostContentRecycler @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 +) : FrostContentView(context, attrs, defStyleAttr, defStyleRes) { + + override val layoutRes: Int = R.layout.view_content_base_recycler + +} + +abstract class FrostContentView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), + FrostContentParent where T : View, T : FrostContentCore { + + private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh) + private val progress: ProgressBar by bindView(R.id.content_progress) + val coreView: T by bindView(R.id.content_core) + + override val core: FrostContentCore + get() = coreView + + override val progressObservable: PublishSubject = PublishSubject.create() + override val refreshObservable: PublishSubject = PublishSubject.create() + override val titleObservable: BehaviorSubject = BehaviorSubject.create() + + override lateinit var baseUrl: String + override var baseEnum: FbItem? = null + + protected abstract val layoutRes: Int + + /** + * Sets up everything + * Called by [bind] + */ + protected fun init() { + inflate(context, layoutRes, this) + coreView.parent = this + + // bind observables + progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { + progress.invisibleIf(it == 100) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + progress.setProgress(it, true) + else + progress.progress = it + } + refreshObservable.observeOn(AndroidSchedulers.mainThread()).subscribe { + refresh.isRefreshing = it + refresh.isEnabled = true + } + refresh.setOnRefreshListener { coreView.reload(true) } + + reloadThemeSelf() + + } + + override fun bind(container: FrostContentContainer) { + baseUrl = container.baseUrl + baseEnum = container.baseEnum + init() + core.bind(container) + } + + override fun reloadTheme() { + reloadThemeSelf() + coreView.reloadTheme() + } + + override fun reloadTextSize() { + coreView.reloadTextSize() + } + + override fun reloadThemeSelf() { + progress.tint(Prefs.textColor.withAlpha(180)) + refresh.setColorSchemeColors(Prefs.iconColor) + refresh.setProgressBackgroundColorSchemeColor(Prefs.headerColor.withAlpha(255)) + } + + override fun reloadTextSizeSelf() { + // intentionally blank + } + + override fun destroy() { + titleObservable.onComplete() + progressObservable.onComplete() + refreshObservable.onComplete() + core.destroy() + } + + /** + * Hook onto the refresh observable for one cycle + * Animate toggles between the fancy ripple and the basic fade + * The cycle only starts on the first load since there may have been another process when this is registered + */ + override fun registerTransition(animate: Boolean) { + with(coreView) { + var dispose: Disposable? = null + var loading = false + dispose = refreshObservable.subscribeOn(AndroidSchedulers.mainThread()).subscribe { + if (it) { + loading = true + if (isVisible) fadeOut(duration = 200L) + } else if (loading) { + dispose?.dispose() + if (animate && Prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) + else fadeIn(duration = 100L) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt new file mode 100644 index 00000000..436f8b00 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -0,0 +1,103 @@ +package com.pitchedapps.frost.views + +import android.content.Context +import android.support.v7.widget.RecyclerView +import android.util.AttributeSet +import android.view.View +import com.pitchedapps.frost.contracts.FrostContentContainer +import com.pitchedapps.frost.contracts.FrostContentCore +import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.fragments.RecyclerContentContract +import com.pitchedapps.frost.utils.L +import java.lang.ref.WeakReference + +/** + * Created by Allan Wang on 2017-05-29. + * + */ +class FrostRecyclerView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr), + FrostContentCore { + + override fun reload(animate: Boolean) = reloadBase(animate) + + override lateinit var parent: FrostContentParent + + override val currentUrl: String + get() = parent.baseUrl + + lateinit var recyclerContract: WeakReference + + override fun bind(container: FrostContentContainer): View { + if (container !is RecyclerContentContract) + throw IllegalStateException("FrostRecyclerView must bind to a container that is a RecyclerContentContract") + this.recyclerContract = WeakReference(container) + container.bind(this) + return this + } + + init { + isNestedScrollingEnabled = true + } + + override fun reloadBase(animate: Boolean) { + val contract = recyclerContract.get() + if (contract == null) { + L.eThrow("Attempted to reload with invalid contract") + return + } + contract.reload({ parent.progressObservable.onNext(it) }) { + parent.progressObservable.onNext(100) + parent.refreshObservable.onNext(false) + } + } + + override fun clearHistory() { + // intentionally blank + } + + override fun destroy() { + // todo see if any + } + + override fun onBackPressed() = false + + /** + * If webview is already at the top, refresh + * Otherwise scroll to top + */ + override fun onTabClicked() { + if (scrollY < 5) reloadBase(true) + else scrollToTop() + } + + private fun scrollToTop() { + stopScroll() + smoothScrollToPosition(0) + } + + override var active: Boolean = true + set(value) { + if (field == value) return + field = value + // todo + } + + override fun reloadTheme() { + reloadThemeSelf() + } + + override fun reloadThemeSelf() { + reload(false) // todo see if there's a better solution + } + + override fun reloadTextSize() { + reloadTextSizeSelf() + } + + override fun reloadTextSizeSelf() { + // todo + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt new file mode 100644 index 00000000..e6e1f0e2 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt @@ -0,0 +1,144 @@ +package com.pitchedapps.frost.views + +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.view.animation.DecelerateInterpolator +import com.pitchedapps.frost.contracts.FrostContentContainer +import com.pitchedapps.frost.contracts.FrostContentCore +import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.facebook.USER_AGENT_BASIC +import com.pitchedapps.frost.fragments.WebFragment +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.frostDownload +import com.pitchedapps.frost.web.* + +/** + * Created by Allan Wang on 2017-05-29. + * + */ +class FrostWebView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : NestedWebView(context, attrs, defStyleAttr), + FrostContentCore { + + override fun reload(animate: Boolean) { + parent.registerTransition(animate) + super.reload() + } + + override lateinit var parent: FrostContentParent + + internal lateinit var frostWebClient: FrostWebViewClient + + override val currentUrl: String + get() = url ?: "" + + @SuppressLint("SetJavaScriptEnabled") + override fun bind(container: FrostContentContainer): View { + with(settings) { + javaScriptEnabled = true + if (parent.baseUrl.shouldUseBasicAgent) + userAgentString = USER_AGENT_BASIC + allowFileAccess = true + textZoom = Prefs.webTextScaling + } + setLayerType(LAYER_TYPE_HARDWARE, null) + // attempt to get custom client; otherwise fallback to original + frostWebClient = (container as? WebFragment)?.client(this) ?: FrostWebViewClient(this) + webViewClient = frostWebClient + webChromeClient = FrostChromeClient(this) + addJavascriptInterface(FrostJSI(this), "Frost") + setBackgroundColor(Color.TRANSPARENT) + setDownloadListener(context::frostDownload) + return this + } + + + /** + * Wrapper to the main userAgentString to cache it. + * This decouples it from the UiThread + * + * Note that this defaults to null, but the main purpose is to + * check if we've set our own agent. + * + * A null value may be interpreted as the default value + */ + var userAgentString: String? = null + set(value) { + field = value + settings.userAgentString = value + } + + init { + isNestedScrollingEnabled = true + } + + fun loadUrl(url: String?, animate: Boolean) { + if (url == null) return + parent.registerTransition(animate) + super.loadUrl(url) + } + + override fun reloadBase(animate: Boolean) { + loadUrl(parent.baseUrl, animate) + } + + override fun onBackPressed(): Boolean { + if (canGoBack()) { + goBack() + return true + } + return false + } + + /** + * If webview is already at the top, refresh + * Otherwise scroll to top + */ + override fun onTabClicked() { + if (scrollY < 5) reloadBase(true) + else scrollToTop() + } + + private fun scrollToTop() { + flingScroll(0, 0) // stop fling + if (scrollY > 10000) { + scrollTo(0, 0) + } else { + ValueAnimator.ofInt(scrollY, 0).apply { + duration = Math.min(scrollY, 500).toLong() + interpolator = DecelerateInterpolator() + addUpdateListener { scrollY = it.animatedValue as Int } + start() + } + } + } + + override var active: Boolean = true + set(value) { + if (field == value) return + field = value + // todo + } + + override fun reloadTheme() { + reloadThemeSelf() + } + + override fun reloadThemeSelf() { + reload(false) // todo see if there's a better solution + } + + override fun reloadTextSize() { + reloadTextSizeSelf() + } + + override fun reloadTextSizeSelf() { + settings.textZoom = Prefs.webTextScaling + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt index 25079834..9edd671b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt @@ -79,7 +79,7 @@ class KeywordItem(val keyword: String) : AbstractItem?) { + override fun bindView(holder: ViewHolder, payloads: MutableList) { super.bindView(holder, payloads) holder.text.text = keyword } -- cgit v1.2.3