From 14185936f46160997ef9eaae92cb3c8eacae93c5 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 30 Jul 2017 15:57:27 -0700 Subject: Intro (#100) * Create base activity * Created some testers * Update theme and fix mess up * Update theme and replace paint * WIP intro drawables * Create intro screens * Clear unnecessary dependencies * Finalize intro panels * Clean up intro * Attack intro to settings * Fix lint * Finalize intro --- .../pitchedapps/frost/activities/BaseActivity.kt | 2 + .../pitchedapps/frost/activities/ImageActivity.kt | 14 +- .../pitchedapps/frost/activities/IntroActivity.kt | 169 +++++++++++++++++++++ .../pitchedapps/frost/activities/LoginActivity.kt | 3 +- .../pitchedapps/frost/activities/MainActivity.kt | 10 ++ .../frost/activities/SettingsActivity.kt | 9 +- .../com/pitchedapps/frost/injectors/CssAssets.kt | 1 + .../pitchedapps/frost/intro/IntroFragmentTheme.kt | 51 +++++++ .../pitchedapps/frost/intro/IntroImageFragments.kt | 112 ++++++++++++++ .../pitchedapps/frost/intro/IntroMainFragments.kt | 135 ++++++++++++++++ .../frost/utils/AnimatedVectorDelegate.kt | 82 ++++++++++ .../kotlin/com/pitchedapps/frost/utils/Showcase.kt | 2 + .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 10 +- .../com/pitchedapps/frost/utils/iab/IABBinder.kt | 26 ++-- .../com/pitchedapps/frost/utils/iab/IABDialogs.kt | 6 +- .../com/pitchedapps/frost/views/BadgedIcon.kt | 2 + 16 files changed, 606 insertions(+), 28 deletions(-) create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt (limited to 'app/src/main/kotlin/com') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt index fd020af1..6806bf24 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt @@ -1,11 +1,13 @@ package com.pitchedapps.frost.activities +import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import com.pitchedapps.frost.R import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.materialDialogThemed import com.pitchedapps.frost.utils.setFrostTheme +import org.jetbrains.anko.contentView /** * Created by Allan Wang on 2017-06-12. diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt index a7c59deb..2e4ae410 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -122,7 +122,7 @@ class ImageActivity : AppCompatActivity() { } else { photo.setImage(ImageSource.uri(it)) fabAction = FabStates.DOWNLOAD - photo.animate().alpha(1f).scaleX(1f).scaleY(1f).withEndAction { fab.show() }.start() + photo.animate().alpha(1f).scaleXY(1f).withEndAction { fab.show() }.start() } }) } else { @@ -283,9 +283,15 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC * If it's in view, give it some animations */ fun update(fab: FloatingActionButton) { - fab.transition { - setIcon(iicon, color = iconColor) - backgroundTintList = ColorStateList.valueOf(backgroundTint) + if (fab.isHidden) { + fab.setIcon(iicon, color = iconColor) + fab.backgroundTintList = ColorStateList.valueOf(backgroundTint) + fab.show() + } else { + fab.fadeScaleTransition { + setIcon(iicon, color = iconColor) + backgroundTintList = ColorStateList.valueOf(backgroundTint) + } } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt new file mode 100644 index 00000000..28b8f466 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/IntroActivity.kt @@ -0,0 +1,169 @@ +package com.pitchedapps.frost.activities + +import android.animation.ValueAnimator +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentPagerAdapter +import android.support.v4.view.ViewPager +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.view.WindowManager +import android.widget.Button +import android.widget.ImageButton +import android.widget.ImageView +import ca.allanwang.kau.ui.views.RippleCanvas +import ca.allanwang.kau.ui.widgets.InkPageIndicator +import ca.allanwang.kau.utils.* +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.pitchedapps.frost.R +import com.pitchedapps.frost.intro.* +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.cookies +import com.pitchedapps.frost.utils.launchNewTask +import org.jetbrains.anko.find + + +/** + * Created by Allan Wang on 2017-07-25. + */ +class IntroActivity : AppCompatActivity(), ViewPager.PageTransformer, ViewPager.OnPageChangeListener { + + val ripple: RippleCanvas by bindView(R.id.intro_ripple) + val viewpager: ViewPager by bindView(R.id.intro_viewpager) + lateinit var adapter: IntroPageAdapter + val indicator: InkPageIndicator by bindView(R.id.intro_indicator) + val skip: Button by bindView(R.id.intro_skip) + val next: ImageButton by bindView(R.id.intro_next) + private var barHasNext = true + + val fragments = listOf( + IntroFragmentWelcome(), + IntroFragmentTheme(), + IntroAccountFragment(), + IntroTabTouchFragment(), + IntroTabContextFragment(), + IntroFragmentEnd() + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_intro) + adapter = IntroPageAdapter(supportFragmentManager, fragments) + viewpager.apply { + setPageTransformer(true, this@IntroActivity) + addOnPageChangeListener(this@IntroActivity) + adapter = this@IntroActivity.adapter + } + indicator.setViewPager(viewpager) + next.setIcon(GoogleMaterial.Icon.gmd_navigate_next) + next.setOnClickListener { + if (barHasNext) viewpager.setCurrentItem(viewpager.currentItem + 1, true) + else finish(next.x + next.pivotX, next.y + next.pivotY) + } + ripple.set(Prefs.bgColor) + theme() + } + + fun theme() { + statusBarColor = Prefs.headerColor + navigationBarColor = Prefs.headerColor + skip.setTextColor(Prefs.textColor) + next.imageTintList = ColorStateList.valueOf(Prefs.textColor) + indicator.setColour(Prefs.textColor) + indicator.invalidate() + fragments.forEach { it.themeFragment() } + } + + /** + * Transformations are mainly handled on a per view basis + * This sifies it by making the first fragment fade out as the second fragment comes in + * All fragments are locked in position + */ + override fun transformPage(page: View, position: Float) { + //only apply to adjacent pages + if ((position < 0 && position > -1) || (position > 0 && position < 1)) { + val pageWidth = page.width + val translateValue = position * -pageWidth + page.translationX = (if (translateValue > -pageWidth) translateValue else 0f) + page.alpha = if (position < 0) 1 + position else 1f + } else { + page.alpha = 1f + page.translationX = 0f + } + + } + + fun finish(x: Float, y: Float) { + val blue = color(R.color.facebook_blue) + window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + ripple.ripple(blue, x, y, 600) { + postDelayed(1000) { finish() } + } + arrayOf(skip, indicator, next, fragments.last().view!!.find(R.id.intro_title), fragments.last().view!!.find(R.id.intro_desc)).forEach { + it.animate().alpha(0f).setDuration(600).start() + } + if (Prefs.textColor != Color.WHITE) { + val f = fragments.last().view!!.find(R.id.intro_image).drawable + ValueAnimator.ofFloat(0f, 1f).apply { + addUpdateListener { + f.setTint(Prefs.textColor.blendWith(Color.WHITE, it.animatedValue as Float)) + } + duration = 600 + start() + } + } + if (Prefs.headerColor != blue) { + ValueAnimator.ofFloat(0f, 1f).apply { + addUpdateListener { + val c = Prefs.headerColor.blendWith(blue, it.animatedValue as Float) + statusBarColor = c + navigationBarColor = c + } + duration = 600 + start() + } + } + } + + override fun finish() { + launchNewTask(MainActivity::class.java, cookies()) + super.finish() + } + + override fun onBackPressed() { + if (viewpager.currentItem > 0) viewpager.setCurrentItem(viewpager.currentItem - 1, true) + else finish() + } + + override fun onPageScrollStateChanged(state: Int) { + + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + fragments[position].onPageScrolled(positionOffset) + if (position + 1 < fragments.size) + fragments[position + 1].onPageScrolled(positionOffset - 1) + } + + override fun onPageSelected(position: Int) { + fragments[position].onPageSelected() + val hasNext = position != fragments.size - 1 + if (barHasNext == hasNext) return + barHasNext = hasNext + next.fadeScaleTransition { + setIcon(if (barHasNext) GoogleMaterial.Icon.gmd_navigate_next else GoogleMaterial.Icon.gmd_done, color = Prefs.textColor) + } + skip.animate().scaleXY(if (barHasNext) 1f else 0f) + } + + class IntroPageAdapter(fm: FragmentManager, private val fragments: List) : FragmentPagerAdapter(fm) { + + override fun getItem(position: Int): Fragment = fragments[position] + + override fun getCount(): Int = fragments.size + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 2dccbeb5..8503145e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -96,7 +96,8 @@ class LoginActivity : BaseActivity() { loadFbCookiesAsync { cookies -> Handler().postDelayed({ - launchNewTask(MainActivity::class.java, ArrayList(cookies), clearStack = true) + launchNewTask(if (Showcase.intro) IntroActivity::class.java else MainActivity::class.java, + ArrayList(cookies), clearStack = true) }, 1000) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index 978659db..a6396b1b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -86,6 +86,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract, var hiddenSearchView: SearchWebView? = null var firstLoadFinished = false set(value) { + if (field && value) return //both vals are already true L.d("First fragment load has finished") field = value if (value && hiddenSearchView == null) { @@ -160,6 +161,14 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract, // } setFrostColors(toolbar, themeWindow = false, headers = arrayOf(tabs, appBar), backgrounds = arrayOf(viewPager)) onCreateBilling() + if (Prefs.installDate < 1501454310304 && Showcase.intro) + materialDialogThemed { + title(R.string.intro_title) + content(R.string.intro_desc) + positiveText(R.string.kau_yes) + negativeText(R.string.kau_no) + onPositive { _, _ -> launchIntroActivity(cookies()) } + } } fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { @@ -383,6 +392,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract, when (item.itemId) { R.id.action_settings -> { val intent = Intent(this, SettingsActivity::class.java) + intent.putParcelableArrayListExtra(EXTRA_COOKIES, cookies()) val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle() startActivityForResult(intent, ACTIVITY_SETTINGS, bundle) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt index d073050b..8455bf1e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt @@ -79,6 +79,11 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() { onClick = { _, _, _ -> kauLaunchAbout(AboutActivity::class.java); true } } + plainText(R.string.replay_intro) { + iicon = GoogleMaterial.Icon.gmd_replay + onClick = {_,_,_-> launchIntroActivity(cookies()); true} + } + if (BuildConfig.DEBUG) { checkbox(R.string.custom_pro, { Prefs.debugPro }, { Prefs.debugPro = it }) } @@ -116,8 +121,6 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() { } } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_settings, menu) toolbar.tint(Prefs.iconColor) @@ -143,7 +146,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() { } fun setFrostResult(flag: Int) { - resultFlag = resultFlag and flag + resultFlag = resultFlag or flag } override fun onDestroy() { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt index 2f2050cc..ac979c85 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt @@ -38,6 +38,7 @@ enum class CssAssets(val folder: String = "themes") : InjectorContract { .replace("\$BT\$", bt) .replace("\$BBT\$", bbt.toRgbaString()) .replace("\$O\$", Prefs.bgColor.withAlpha(255).toRgbaString()) + .replace("\$OO\$", Prefs.bgColor.colorToForeground(0.35f).withAlpha(255).toRgbaString()) .replace("\$D\$", Prefs.textColor.adjustAlpha(0.3f).toRgbaString()) } injector = JsBuilder().css(content).build() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt new file mode 100644 index 00000000..d1d64712 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt @@ -0,0 +1,51 @@ +package com.pitchedapps.frost.intro + +import android.os.Bundle +import android.view.View +import ca.allanwang.kau.utils.bindViewResettable +import ca.allanwang.kau.utils.scaleXY +import com.pitchedapps.frost.R +import com.pitchedapps.frost.activities.IntroActivity +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.Theme + +/** + * Created by Allan Wang on 2017-07-28. + */ +class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) { + + val light: View by bindViewResettable(R.id.intro_theme_light) + val dark: View by bindViewResettable(R.id.intro_theme_dark) + val amoled: View by bindViewResettable(R.id.intro_theme_amoled) + val glass: View by bindViewResettable(R.id.intro_theme_glass) + + val themeList + get() = listOf(light, dark, amoled, glass) + + override fun viewArray(): Array> + = arrayOf(arrayOf(title), arrayOf(light, dark), arrayOf(amoled, glass)) + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + light.setThemeClick(Theme.LIGHT) + dark.setThemeClick(Theme.DARK) + amoled.setThemeClick(Theme.AMOLED) + glass.setThemeClick(Theme.GLASS) + val currentTheme = Prefs.theme - 1 + if (currentTheme in 0..3) + themeList.forEachIndexed { index, v -> v.scaleXY = if (index == currentTheme) 1.6f else 0.8f } + } + + private fun View.setThemeClick(theme: Theme) { + setOnClickListener { + v -> + Prefs.theme = theme.ordinal + (activity as IntroActivity).apply { + ripple.ripple(Prefs.bgColor, v.x + v.pivotX, v.y + v.pivotY) + theme() + } + themeList.forEach { it.animate().scaleXY(if (it == this) 1.6f else 0.8f).start() } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt new file mode 100644 index 00000000..d19a488d --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt @@ -0,0 +1,112 @@ +package com.pitchedapps.frost.intro + +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.os.Bundle +import android.view.View +import ca.allanwang.kau.utils.colorToForeground +import ca.allanwang.kau.utils.tint +import ca.allanwang.kau.utils.withAlpha +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.Prefs + +/** + * Created by Allan Wang on 2017-07-28. + */ +abstract class BaseImageIntroFragment(val titleRes: Int, val imageRes: Int, val descRes: Int) : BaseIntroFragment(R.layout.intro_image) { + + val imageDrawable: LayerDrawable by lazyResettableRegistered { image.drawable as LayerDrawable } + val phone: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone) } + val screen: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen) } + + override fun viewArray(): Array> + = arrayOf(arrayOf(title), arrayOf(desc)) + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + title.setText(titleRes) + image.setImageResource(imageRes) + desc.setText(descRes) + super.onViewCreated(view, savedInstanceState) + } + + override fun themeFragmentImpl() { + super.themeFragmentImpl() + title.setTextColor(Prefs.textColor) + desc.setTextColor(Prefs.textColor) + phone.tint(Prefs.textColor) + screen.tint(Prefs.bgColor) + } + + fun themeImageComponent(color: Int, vararg id: Int) { + id.forEach { imageDrawable.findDrawableByLayerId(it).tint(color) } + } + + override fun onPageScrolledImpl(positionOffset: Float) { + super.onPageScrolledImpl(positionOffset) + val alpha = ((1 - Math.abs(positionOffset)) * 255).toInt() + //apply alpha to all layers except the phone base + (0 until imageDrawable.numberOfLayers).forEach { + val d = imageDrawable.getDrawable(it) + if (d != phone) d.alpha = alpha + } + } + + fun firstImageFragmentTransition(offset: Float) { + if (offset < 0) + image.alpha = 1 + offset + } + + fun lastImageFragmentTransition(offset: Float) { + if (offset > 0) + image.alpha = 1 - offset + } +} + +class IntroAccountFragment : BaseImageIntroFragment( + R.string.intro_multiple_accounts, R.drawable.intro_phone_nav, R.string.intro_multiple_accounts_desc +) { + + override fun themeFragmentImpl() { + super.themeFragmentImpl() + themeImageComponent(Prefs.iconColor, R.id.intro_phone_avatar_1, R.id.intro_phone_avatar_2) + themeImageComponent(Prefs.bgColor.colorToForeground(), R.id.intro_phone_nav) + themeImageComponent(Prefs.headerColor, R.id.intro_phone_header) + } + + override fun onPageScrolledImpl(positionOffset: Float) { + super.onPageScrolledImpl(positionOffset) + firstImageFragmentTransition(positionOffset) + } +} + +class IntroTabTouchFragment : BaseImageIntroFragment( + R.string.intro_easy_navigation, R.drawable.intro_phone_tab, R.string.intro_easy_navigation_desc +) { + + override fun themeFragmentImpl() { + super.themeFragmentImpl() + themeImageComponent(Prefs.iconColor, R.id.intro_phone_icon_1, R.id.intro_phone_icon_2, R.id.intro_phone_icon_3, R.id.intro_phone_icon_4) + themeImageComponent(Prefs.headerColor, R.id.intro_phone_tab) + themeImageComponent(Prefs.textColor.withAlpha(80), R.id.intro_phone_icon_ripple) + } +} + +class IntroTabContextFragment : BaseImageIntroFragment( + R.string.intro_context_aware, R.drawable.intro_phone_long_press, R.string.intro_context_aware_desc +) { + + override fun themeFragmentImpl() { + super.themeFragmentImpl() + themeImageComponent(Prefs.headerColor, R.id.intro_phone_toolbar) + themeImageComponent(Prefs.bgColor.colorToForeground(0.1f), R.id.intro_phone_image) + themeImageComponent(Prefs.bgColor.colorToForeground(0.2f), R.id.intro_phone_like, R.id.intro_phone_share) + themeImageComponent(Prefs.bgColor.colorToForeground(0.3f), R.id.intro_phone_comment) + themeImageComponent(Prefs.bgColor.colorToForeground(0.1f), R.id.intro_phone_card_1, R.id.intro_phone_card_2) + themeImageComponent(Prefs.textColor, R.id.intro_phone_image_indicator, R.id.intro_phone_comment_indicator, R.id.intro_phone_card_indicator) + } + + override fun onPageScrolledImpl(positionOffset: Float) { + super.onPageScrolledImpl(positionOffset) + lastImageFragmentTransition(positionOffset) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt new file mode 100644 index 00000000..552fad3b --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroMainFragments.kt @@ -0,0 +1,135 @@ +package com.pitchedapps.frost.intro + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.os.Bundle +import android.support.constraint.ConstraintLayout +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import ca.allanwang.kau.kotlin.LazyResettableRegistry +import ca.allanwang.kau.utils.Kotterknife +import ca.allanwang.kau.utils.bindViewResettable +import ca.allanwang.kau.utils.setOnSingleTapListener +import com.pitchedapps.frost.R +import com.pitchedapps.frost.activities.IntroActivity +import com.pitchedapps.frost.utils.Prefs +import org.jetbrains.anko.childrenSequence + +/** + * Created by Allan Wang on 2017-07-28. + * + * Contains the base, start, and end fragments + */ + +/** + * The core intro fragment for all other fragments + */ +abstract class BaseIntroFragment(val layoutRes: Int) : Fragment() { + + val screenWidth + get() = resources.displayMetrics.widthPixels + + val lazyRegistry = LazyResettableRegistry() + + protected fun translate(offset: Float, views: Array>) { + val maxTranslation = offset * screenWidth + val increment = maxTranslation / views.size + views.forEachIndexed { i, group -> + group.forEach { + it.translationX = if (offset > 0) -maxTranslation + i * increment else -(i + 1) * increment + it.alpha = 1 - Math.abs(offset) + } + } + } + + fun lazyResettableRegistered(initializer: () -> T) = lazyRegistry.lazy(initializer) + + /* + * Note that these ids aren't actually inside all layouts + * However, they are in most of them, so they are added here + * for convenience + */ + protected val title: TextView by bindViewResettable(R.id.intro_title) + protected val image: ImageView by bindViewResettable(R.id.intro_image) + protected val desc: TextView by bindViewResettable(R.id.intro_desc) + + protected fun defaultViewArray(): Array> = arrayOf(arrayOf(title), arrayOf(image), arrayOf(desc)) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(layoutRes, container, false) + return view + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + themeFragment() + } + + override fun onDestroyView() { + super.onDestroyView() + Kotterknife.reset(this) + lazyRegistry.invalidateAll() + } + + fun themeFragment() { + if (view != null) themeFragmentImpl() + } + + protected open fun themeFragmentImpl() { + view?.childrenSequence()?.forEach { (it as? TextView)?.setTextColor(Prefs.textColor) } + } + + protected val viewArray: Array> by lazyResettableRegistered { viewArray() } + + protected abstract fun viewArray(): Array> + + fun onPageScrolled(positionOffset: Float) { + if (view != null) onPageScrolledImpl(positionOffset) + } + + protected open fun onPageScrolledImpl(positionOffset: Float) { + translate(positionOffset, viewArray) + } + + fun onPageSelected() { + if (view != null) onPageSelectedImpl() + } + + protected open fun onPageSelectedImpl() { + + } +} + +class IntroFragmentWelcome : BaseIntroFragment(R.layout.intro_welcome) { + + override fun viewArray(): Array> = defaultViewArray() + + override fun themeFragmentImpl() { + super.themeFragmentImpl() + image.imageTintList = ColorStateList.valueOf(Prefs.textColor) + } +} + +class IntroFragmentEnd : BaseIntroFragment(R.layout.intro_end) { + + val container: ConstraintLayout by bindViewResettable(R.id.intro_end_container) + + override fun viewArray(): Array> = defaultViewArray() + + override fun themeFragmentImpl() { + super.themeFragmentImpl() + image.imageTintList = ColorStateList.valueOf(Prefs.textColor) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + container.setOnSingleTapListener { _, event -> + (activity as IntroActivity).finish(event.x, event.y) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt new file mode 100644 index 00000000..a530df32 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt @@ -0,0 +1,82 @@ +package com.pitchedapps.frost.utils + +import android.graphics.drawable.AnimatedVectorDrawable +import android.support.annotation.DrawableRes +import android.widget.ImageView +import ca.allanwang.kau.utils.drawable + +/** + * Created by Allan Wang on 2017-07-29. + * + * Delegate for animated vector drawables with two states (start and end) + * Drawables are added lazily depending on the animation direction, and are verified upon load + * Should the bounded view not have an animated drawable upon animating, it is assumed + * that the user has switched the resource themselves and the delegate will not switch the resource + */ +interface AnimatedVectorContract { + fun animate() + fun animateReverse() + fun animateToggle() + val isAtStart: Boolean + fun bind(view: ImageView) + var animatedVectorListener: ((avd: AnimatedVectorDrawable, forwards: Boolean) -> Unit)? +} + +class AnimatedVectorDelegate( + /** + * The res for the starting resource; must have parent tag animated-vector + */ + @param:DrawableRes val avdStart: Int, + /** + * The res for the ending resource; must have parent tag animated-vector + */ + @param:DrawableRes val avdEnd: Int, + /** + * The delegate will automatically set the start resource when bound + * If [emitOnBind] is true, it will also trigger the listener + */ + val emitOnBind: Boolean = true, + /** + * The optional listener that will be triggered every time the avd is switched by the delegate + */ + override var animatedVectorListener: ((avd: AnimatedVectorDrawable, forwards: Boolean) -> Unit)? = null +) : AnimatedVectorContract { + + lateinit var view: ImageView + + private var atStart = true + + override val isAtStart: Boolean + get() = atStart + + private val avd: AnimatedVectorDrawable? + get() = view.drawable as? AnimatedVectorDrawable + + override fun bind(view: ImageView) { + this.view = view + view.context.drawable(avdStart) as? AnimatedVectorDrawable ?: throw IllegalArgumentException("AnimatedVectorDelegate has a starting drawable that isn't an avd") + view.context.drawable(avdEnd) as? AnimatedVectorDrawable ?: throw IllegalArgumentException("AnimatedVectorDelegate has an ending drawable that isn't an avd") + view.setImageResource(avdStart) + if (emitOnBind) animatedVectorListener?.invoke(avd!!, false) + } + + override fun animate() = animateImpl(false) + + override fun animateReverse() = animateImpl(true) + + override fun animateToggle() = animateImpl(!atStart) + + private fun animateImpl(toStart: Boolean) { + if ((atStart == toStart)) return L.d("AVD already at ${if (toStart) "start" else "end"}") + if (avd == null) return L.d("AVD null resource")//no longer using animated vector; do not modify + avd?.stop() + view.setImageResource(if (toStart) avdEnd else avdStart) + animatedVectorListener?.invoke(avd!!, !toStart) + atStart = toStart + avd?.start() + } + +} + + + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Showcase.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Showcase.kt index 57cbef7e..b3601dfb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Showcase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Showcase.kt @@ -14,6 +14,8 @@ object Showcase : KPref() { //check if this is the first time launching the web overlay; show snackbar if true val firstWebOverlay: Boolean by kprefSingle("first_web_overlay") + val intro: Boolean by kprefSingle("intro_pages") + //not a showcase but cannot be in the same file as Prefs var experimentalDefault: Boolean by kpref("experimental_by_default", false) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index 442216fb..40e16f20 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -20,10 +20,7 @@ import com.crashlytics.android.answers.Answers import com.crashlytics.android.answers.CustomEvent import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R -import com.pitchedapps.frost.activities.ImageActivity -import com.pitchedapps.frost.activities.LoginActivity -import com.pitchedapps.frost.activities.SelectorActivity -import com.pitchedapps.frost.activities.WebOverlayActivity +import com.pitchedapps.frost.activities.* import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.formattedFbUrl @@ -68,6 +65,9 @@ fun Context.launchImageActivity(imageUrl: String, text: String?) { }) } +fun Activity.launchIntroActivity(cookieList: ArrayList) + = launchNewTask(IntroActivity::class.java, cookieList, true) + fun WebOverlayActivity.url(): String { return intent.extras?.getString(ARG_URL) ?: FbTab.FEED.url } @@ -140,4 +140,4 @@ fun Activity.frostNavigationBar() { navigationBarColor = if (Prefs.tintNavBar) Prefs.headerColor else Color.BLACK } -fun RequestBuilder.withRoundIcon() = apply(RequestOptions().transform(CircleCrop())) \ No newline at end of file +fun RequestBuilder.withRoundIcon() = apply(RequestOptions().transform(CircleCrop()))!! \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt index ab9e37d1..b3992ff4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt @@ -34,7 +34,7 @@ abstract class IABBinder : FrostBilling { override fun Activity.onCreateBilling() { activity = this bp = BillingProcessor.newBillingProcessor(this, PUBLIC_BILLING_KEY, this@IABBinder) - bp!!.initialize() + bp?.initialize() } override fun onDestroyBilling() { @@ -79,10 +79,10 @@ abstract class IABBinder : FrostBilling { L.eThrow("IAB null bp on purchase attempt") return } - if (!bp!!.isOneTimePurchaseSupported) - activity!!.playStorePurchaseUnsupported() + if (!(bp?.isOneTimePurchaseSupported ?: false)) + activity?.playStorePurchaseUnsupported() else - bp!!.purchase(activity, FROST_PRO) + bp?.purchase(activity, FROST_PRO) } } @@ -104,14 +104,14 @@ class IABSettings : IABBinder() { */ override fun restorePurchases() { if (bp == null) return - val load = bp!!.loadOwnedPurchasesFromGoogle() + val load = bp?.loadOwnedPurchasesFromGoogle() ?: return L.d("IAB settings load from google $load") - if (!bp!!.isPurchased(FROST_PRO)) { - if (Prefs.pro) activity!!.playStoreNoLongerPro() + if (!(bp?.isPurchased(FROST_PRO) ?: return)) { + if (Prefs.pro) activity.playStoreNoLongerPro() else purchasePro() } else { - if (!Prefs.pro) activity!!.playStoreFoundPro() - else activity!!.purchaseRestored() + if (!Prefs.pro) activity.playStoreFoundPro() + else activity?.purchaseRestored() } } } @@ -138,12 +138,12 @@ class IABMain : IABBinder() { override fun restorePurchases() { if (restored || bp == null) return restored = true - val load = bp!!.loadOwnedPurchasesFromGoogle() + val load = bp?.loadOwnedPurchasesFromGoogle() ?: false L.d("IAB main load from google $load") - if (!bp!!.isPurchased(FROST_PRO)) { - if (Prefs.pro) activity!!.playStoreNoLongerPro() + if (!(bp?.isPurchased(FROST_PRO) ?: false)) { + if (Prefs.pro) activity.playStoreNoLongerPro() } else { - if (!Prefs.pro) activity!!.playStoreFoundPro() + if (!Prefs.pro) activity.playStoreFoundPro() } onDestroyBilling() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt index d2f22829..df0f04fd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt @@ -31,7 +31,7 @@ private fun Activity.playRestart() { } else restart() } -fun Activity.playStoreNoLongerPro() { +fun Activity?.playStoreNoLongerPro() { Prefs.pro = false L.d("IAB No longer pro") frostAnswers { @@ -39,6 +39,7 @@ fun Activity.playStoreNoLongerPro() { .putCustomAttribute("result", "no longer pro") .putSuccess(false)) } + if (this == null) return materialDialogThemed { title(R.string.uh_oh) content(R.string.play_store_not_pro) @@ -49,9 +50,10 @@ fun Activity.playStoreNoLongerPro() { } } -fun Activity.playStoreFoundPro() { +fun Activity?.playStoreFoundPro() { Prefs.pro = true L.d("Found pro") + if (this == null) return materialDialogThemed { title(R.string.found_pro) content(R.string.found_pro_desc) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/BadgedIcon.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/BadgedIcon.kt index 8ae54ef3..df468715 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/BadgedIcon.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/BadgedIcon.kt @@ -1,6 +1,8 @@ package com.pitchedapps.frost.views import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable import android.graphics.drawable.GradientDrawable import android.support.constraint.ConstraintLayout import android.util.AttributeSet -- cgit v1.2.3