diff --git a/app/build.gradle b/app/build.gradle index 9c6e3ab..9cfeffc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-android' android { compileSdkVersion 28 @@ -9,7 +11,6 @@ android { targetSdkVersion 28 versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -23,9 +24,10 @@ dependencies { implementation project(':library') implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { - exclude group: 'com.android.support', module: 'support-annotations' - }) + implementation 'com.ToxicBakery.viewpager.transforms:view-pager-transforms:2.0.24' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.jakewharton.timber:timber:4.7.1' +} +repositories { + mavenCentral() } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9505e53..64c21af 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,16 +1,16 @@ - - + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> @@ -20,4 +20,6 @@ + + \ No newline at end of file diff --git a/app/src/main/java/jp/shts/android/storyprogressbar/MainActivity.java b/app/src/main/java/jp/shts/android/storyprogressbar/MainActivity.java deleted file mode 100644 index 986b519..0000000 --- a/app/src/main/java/jp/shts/android/storyprogressbar/MainActivity.java +++ /dev/null @@ -1,114 +0,0 @@ -package jp.shts.android.storyprogressbar; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowManager; -import android.widget.ImageView; - -import jp.shts.android.storiesprogressview.StoriesProgressView; - -public class MainActivity extends AppCompatActivity implements StoriesProgressView.StoriesListener { - - private static final int PROGRESS_COUNT = 6; - - private StoriesProgressView storiesProgressView; - private ImageView image; - - private int counter = 0; - private final int[] resources = new int[]{ - R.drawable.sample1, - R.drawable.sample2, - R.drawable.sample3, - R.drawable.sample4, - R.drawable.sample5, - R.drawable.sample6, - }; - - private final long[] durations = new long[]{ - 500L, 1000L, 1500L, 4000L, 5000L, 1000, - }; - - long pressTime = 0L; - long limit = 500L; - - private View.OnTouchListener onTouchListener = new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - pressTime = System.currentTimeMillis(); - storiesProgressView.pause(); - return false; - case MotionEvent.ACTION_UP: - long now = System.currentTimeMillis(); - storiesProgressView.resume(); - return limit < now - pressTime; - } - return false; - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - setContentView(R.layout.activity_main); - - storiesProgressView = (StoriesProgressView) findViewById(R.id.stories); - storiesProgressView.setStoriesCount(PROGRESS_COUNT); - storiesProgressView.setStoryDuration(3000L); - // or - // storiesProgressView.setStoriesCountWithDurations(durations); - storiesProgressView.setStoriesListener(this); -// storiesProgressView.startStories(); - counter = 2; - storiesProgressView.startStories(counter); - - image = (ImageView) findViewById(R.id.image); - image.setImageResource(resources[counter]); - - // bind reverse view - View reverse = findViewById(R.id.reverse); - reverse.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - storiesProgressView.reverse(); - } - }); - reverse.setOnTouchListener(onTouchListener); - - // bind skip view - View skip = findViewById(R.id.skip); - skip.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - storiesProgressView.skip(); - } - }); - skip.setOnTouchListener(onTouchListener); - } - - @Override - public void onNext() { - image.setImageResource(resources[++counter]); - } - - @Override - public void onPrev() { - if ((counter - 1) < 0) return; - image.setImageResource(resources[--counter]); - } - - @Override - public void onComplete() { - } - - @Override - protected void onDestroy() { - // Very important ! - storiesProgressView.destroy(); - super.onDestroy(); - } -} diff --git a/app/src/main/java/jp/shts/android/storyprogressbar/MainActivity.kt b/app/src/main/java/jp/shts/android/storyprogressbar/MainActivity.kt new file mode 100644 index 0000000..ffc8cb8 --- /dev/null +++ b/app/src/main/java/jp/shts/android/storyprogressbar/MainActivity.kt @@ -0,0 +1,141 @@ +package jp.shts.android.storyprogressbar + +import android.animation.Animator +import android.animation.ValueAnimator +import android.os.Bundle +import android.util.SparseIntArray +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.viewpager.widget.ViewPager +import com.ToxicBakery.viewpager.transforms.CubeOutTransformer +import timber.log.Timber + +interface PageViewOperator { + fun backPageView() + fun nextPageView() +} + +class MainActivity : AppCompatActivity(), PageViewOperator { + + companion object { + private const val PAGE_COUNT = 5 + /* key: page-count, value: story-count */ + val progressState = SparseIntArray() + } + + private lateinit var viewPager: ViewPager + private lateinit var pageAdapter: PageAdapter + + private var currentPage: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + setContentView(R.layout.activity_main) + + Timber.plant(Timber.DebugTree()) + + pageAdapter = PageAdapter(supportFragmentManager) + viewPager = findViewById(R.id.viewpager) + viewPager.adapter = pageAdapter + viewPager.setPageTransformer(true, CubeOutTransformer()) + viewPager.addOnPageChangeListener(object : PageChangeListener() { + override fun onPageScrollCanceled() { + Timber.d("onPageScrollCanceled()") + currentFragment()?.resume() + } + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + currentPage = position +// currentFragment()?.resume() + } + }) + } + + private fun currentFragment(): StoryFragment? { + return pageAdapter.findFragmentByPosition(viewPager, currentPage) as StoryFragment + } + + override fun backPageView() { + if (viewPager.currentItem > 0) { + fakeDrag(false) + } + } + + override fun nextPageView() { + if (viewPager.currentItem + 1 < viewPager.adapter?.count ?: 0) { + fakeDrag(true) + } + } + + private var prevDragPosition = 0 + + /** + * Change ViewPage sliding programmatically(not using reflection). + * https://tech.dely.jp/entry/2018/12/13/110000 + * What for? + * setCurrentItem(int, boolean) changes too fast. And it cannot set animation duration. + */ + private fun fakeDrag(forward: Boolean) { + if (prevDragPosition == 0 && viewPager.beginFakeDrag()) { + ValueAnimator.ofInt(0, viewPager.width).apply { + duration = 500L + interpolator = FastOutSlowInInterpolator() + addListener(object : Animator.AnimatorListener { + + override fun onAnimationStart(animation: Animator?) = Unit + + override fun onAnimationEnd(animation: Animator?) { + removeAllUpdateListeners() + if (viewPager.isFakeDragging) { + viewPager.endFakeDrag() + } + prevDragPosition = 0 + } + + override fun onAnimationCancel(animation: Animator?) { + removeAllUpdateListeners() + if (viewPager.isFakeDragging) { + viewPager.endFakeDrag() + } + prevDragPosition = 0 + } + + override fun onAnimationRepeat(animation: Animator?) = Unit + }) + addUpdateListener { + if (!viewPager.isFakeDragging) { + return@addUpdateListener + } + val dragPosition: Int = it.animatedValue as Int + val dragOffset: Float = ((dragPosition - prevDragPosition) * if (forward) -1 else 1).toFloat() + prevDragPosition = dragPosition + viewPager.fakeDragBy(dragOffset) + } + }.start() + } + } + + private class PageAdapter internal constructor(fragmentManager: FragmentManager) : + FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getItem(position: Int): Fragment = StoryFragment.createIntent(position) + override fun getCount(): Int { + return PAGE_COUNT + } + /** + * https://qiita.com/chooblarin/items/88b4accac0cbb6944d4b#%E6%96%B9%E6%B3%953-instantiateitem%E3%82%92%E4%BD%BF%E3%81%86 + */ + fun findFragmentByPosition(viewPager: ViewPager, position: Int): Fragment? { + try { + val f = instantiateItem(viewPager, position) + return f as? Fragment + } finally { + finishUpdate(viewPager) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/jp/shts/android/storyprogressbar/PageChangeListener.kt b/app/src/main/java/jp/shts/android/storyprogressbar/PageChangeListener.kt new file mode 100644 index 0000000..2810c8a --- /dev/null +++ b/app/src/main/java/jp/shts/android/storyprogressbar/PageChangeListener.kt @@ -0,0 +1,54 @@ +package jp.shts.android.storyprogressbar + +import android.os.Handler +import android.os.HandlerThread +import android.os.Message +import androidx.viewpager.widget.ViewPager.* +import timber.log.Timber + +abstract class PageChangeListener : OnPageChangeListener { + + companion object { + private const val DEBOUNCE_TIMES = 500L + } + + private var pageBeforeDragging = 0 + private var currentPage = 0 + private var lastTime = DEBOUNCE_TIMES + 1L + + override fun onPageScrollStateChanged(state: Int) { + when (state) { + SCROLL_STATE_IDLE -> { + Timber.d("onPageScrollStateChanged(): SCROLL_STATE_IDLE") + // 500ms 以下間隔のリクエストは破棄する + val now = System.currentTimeMillis() + if (now - lastTime < DEBOUNCE_TIMES) { + return + } + lastTime = now + Handler().postDelayed({ + if (pageBeforeDragging == currentPage) { + onPageScrollCanceled() + } + }, 300L) + } + SCROLL_STATE_DRAGGING -> { + Timber.d("onPageScrollStateChanged(): SCROLL_STATE_DRAGGING") + pageBeforeDragging = currentPage + } + SCROLL_STATE_SETTLING -> { + Timber.d("onPageScrollStateChanged(): SCROLL_STATE_SETTLING") + } + } + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + } + + override fun onPageSelected(position: Int) { + Timber.d("onPageSelected(): position($position)") + currentPage = position + } + + abstract fun onPageScrollCanceled() +} diff --git a/app/src/main/java/jp/shts/android/storyprogressbar/StoryFragment.kt b/app/src/main/java/jp/shts/android/storyprogressbar/StoryFragment.kt new file mode 100644 index 0000000..0b39eab --- /dev/null +++ b/app/src/main/java/jp/shts/android/storyprogressbar/StoryFragment.kt @@ -0,0 +1,190 @@ +package jp.shts.android.storyprogressbar + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.* +import androidx.fragment.app.Fragment + +import android.widget.ImageView + +import jp.shts.android.storiesprogressview.StoriesProgressView +import timber.log.Timber + +class StoryFragment : Fragment(), StoriesProgressView.StoriesListener { + + companion object { + private const val PROGRESS_COUNT = 6 + private const val EXTRA_POSITION = "extra_position" + + fun createIntent(position: Int): StoryFragment { + return StoryFragment().apply { + arguments = Bundle().apply { + putInt(EXTRA_POSITION, position) + } + } + } + } + + private var storiesProgressView: StoriesProgressView? = null + private var image: ImageView? = null + + private var counter = 0 + private val resources = intArrayOf( + R.drawable.sample1, + R.drawable.sample2, + R.drawable.sample3, + R.drawable.sample4, + R.drawable.sample5, + R.drawable.sample6 + ) + + private val durations = longArrayOf(500L, 1000L, 1500L, 4000L, 5000L, 1000) + + private var pressTime = 0L + private var limit = 500L + + private val onTouchListener = View.OnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + pressTime = System.currentTimeMillis() + storiesProgressView?.pause() + return@OnTouchListener false + } + MotionEvent.ACTION_UP -> { + val now = System.currentTimeMillis() + storiesProgressView?.resume() + return@OnTouchListener limit < now - pressTime + } + } + false + } + + private var pageViewOperator: PageViewOperator? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is PageViewOperator) { + this.pageViewOperator = context + } + } + + override fun onStart() { + super.onStart() + Timber.v("${arguments?.getInt(EXTRA_POSITION)}: onStart") + counter = restorePosition() + } + + override fun onStop() { + super.onStop() + Timber.v("${arguments?.getInt(EXTRA_POSITION)}: onStop") + } + + override fun onResume() { + super.onResume() + Timber.d("${arguments?.getInt(EXTRA_POSITION)}: onResume") + if (counter == 0) { + // start animation + storiesProgressView?.startStories() + } else { + // restart animation + counter = MainActivity.progressState.get(arguments?.getInt(EXTRA_POSITION) ?: 0) + Timber.d("startStories via onResume(): cp(${arguments?.getInt(EXTRA_POSITION)}) c($counter)") + storiesProgressView?.startStories(counter) + } + } + + override fun onPause() { + super.onPause() + Timber.v("${arguments?.getInt(EXTRA_POSITION)}: onPause") +// storiesProgressView?.pause() + storiesProgressView?.abandon() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + counter = restorePosition() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fragment_story, container, false) + storiesProgressView = view.findViewById(R.id.stories) + storiesProgressView?.setStoriesCountDebug(PROGRESS_COUNT, arguments?.getInt(EXTRA_POSITION) ?: -1) + storiesProgressView?.setStoryDuration(3000L) + storiesProgressView?.setStoriesListener(this) + + // bind image + image = view.findViewById(R.id.image) + image?.setImageResource(resources[counter]) + image?.setOnTouchListener(onTouchListener) + + // bind reverse view + val reverse = view.findViewById(R.id.reverse) + reverse.setOnClickListener { + if (counter == 0) { + pageViewOperator?.backPageView() + } else { + storiesProgressView?.reverse() + } + } + reverse.setOnTouchListener(onTouchListener) + + // bind skip view + val skip = view.findViewById(R.id.skip) + skip.setOnClickListener { + Timber.d("counter($counter) resources.size(${resources.size})") + if (counter == resources.size - 1) { + pageViewOperator?.nextPageView() + } else { + storiesProgressView?.skip() + } + } + skip.setOnTouchListener(onTouchListener) + return view + } + + fun resume() { + Timber.d("startStories via onPageScrollCanceled(): cp(${arguments?.getInt(EXTRA_POSITION)}) c($counter)") + Timber.d("${arguments?.getInt(EXTRA_POSITION)}: resume") + storiesProgressView?.resume() + } + + override fun onDestroyView() { + Timber.w("${arguments?.getInt(EXTRA_POSITION)}: onDestroyView") + savePosition(counter) + // Very important ! + storiesProgressView?.destroy() + super.onDestroyView() + } + + private fun savePosition(position: Int) { + MainActivity.progressState.put(arguments!!.getInt(EXTRA_POSITION), position) + } + + private fun restorePosition(): Int { + return MainActivity.progressState.get(arguments!!.getInt(EXTRA_POSITION)) + } + + /* implement StoriesProgressView.StoriesListener start */ + override fun onNext() { + Timber.d("onNext(): counter($counter)") + if (resources.size < counter + 1) { + return + } + savePosition(counter + 1) + image?.setImageResource(resources[++counter]) + } + + override fun onPrev() { + if (counter - 1 < 0) { + return + } + savePosition(counter - 1) + image?.setImageResource(resources[--counter]) + } + + override fun onComplete() { + pageViewOperator?.nextPageView() + } + /* implement StoriesProgressView.StoriesListener end */ +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6568439..a65147a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,41 +1,5 @@ - - - - - - - - - - - - - \ No newline at end of file + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..2608743 --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/fragment_story.xml b/app/src/main/res/layout/fragment_story.xml new file mode 100644 index 0000000..3307bfc --- /dev/null +++ b/app/src/main/res/layout/fragment_story.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 68e19b3..3f35f09 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.61' repositories { jcenter() mavenCentral() @@ -9,6 +10,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/library/src/main/java/jp/shts/android/storiesprogressview/PausableProgressBar.java b/library/src/main/java/jp/shts/android/storiesprogressview/PausableProgressBar.java index af7e472..b0b4563 100644 --- a/library/src/main/java/jp/shts/android/storiesprogressview/PausableProgressBar.java +++ b/library/src/main/java/jp/shts/android/storiesprogressview/PausableProgressBar.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; @@ -13,8 +14,12 @@ import android.view.animation.Transformation; import android.widget.FrameLayout; +import java.util.UUID; + final class PausableProgressBar extends FrameLayout { + private static final String TAG = StoriesProgressView.class.getSimpleName(); + /*** * progress満了タイマーのデフォルト時間 */ @@ -95,7 +100,10 @@ private void finishProgress(boolean isMax) { } } + private boolean isStarted = false; + public void startProgress() { + Log.v(TAG, getTag() + ": startProgress"); maxProgressView.setVisibility(GONE); animation = new PausableScaleAnimation(0, 1, 1, 1, Animation.ABSOLUTE, 0, Animation.RELATIVE_TO_SELF, 0); @@ -104,6 +112,11 @@ public void startProgress() { animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { + if (isStarted) { + return; + } + isStarted = true; + Log.d(TAG, getTag() + ": onAnimationStart"); frontProgressView.setVisibility(View.VISIBLE); if (callback != null) callback.onStartProgress(); } @@ -114,6 +127,8 @@ public void onAnimationRepeat(Animation animation) { @Override public void onAnimationEnd(Animation animation) { + isStarted = false; + Log.d(TAG, getTag() + ": onAnimationEnd"); if (callback != null) callback.onFinishProgress(); } }); diff --git a/library/src/main/java/jp/shts/android/storiesprogressview/StoriesProgressView.java b/library/src/main/java/jp/shts/android/storiesprogressview/StoriesProgressView.java index ec8c22e..fa6b66e 100644 --- a/library/src/main/java/jp/shts/android/storiesprogressview/StoriesProgressView.java +++ b/library/src/main/java/jp/shts/android/storiesprogressview/StoriesProgressView.java @@ -7,11 +7,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.widget.LinearLayout; import java.util.ArrayList; import java.util.List; +import java.util.Timer; public class StoriesProgressView extends LinearLayout { @@ -75,6 +77,7 @@ private void bindViews() { for (int i = 0; i < storiesCount; i++) { final PausableProgressBar p = createProgressBar(); + p.setTag("p(" + position + ") c(" + i + ")"); // debug progressBars.add(p); addView(p); if ((i + 1) < storiesCount) { @@ -94,6 +97,12 @@ private View createSpace() { v.setLayoutParams(SPACE_LAYOUT_PARAM); return v; } +private int position = -1; + public void setStoriesCountDebug(int storiesCount, int position) { + this.storiesCount = storiesCount; + this.position = position; + bindViews(); + } /** * Set story count and create views @@ -209,6 +218,10 @@ public void startStories() { * Start progress animation from specific progress */ public void startStories(int from) { + Log.w(TAG, getTag() + ": startStories start from(" + from + ")"); + for (int i = 0; i < progressBars.size(); i++) { + progressBars.get(i).clear(); + } for (int i = 0; i < from; i++) { progressBars.get(i).setMaxWithoutCallback(); } @@ -224,6 +237,10 @@ public void destroy() { } } + public void abandon() { + progressBars.get(current).setMinWithoutCallback(); + } + /** * Pause story */