Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loadable slider #139

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ sta.setReversed(true);

#### ``slider_icon``

You can set a custom icon by setting the ``slider_icon``attribute to a drawable resource.
You can set a custom icon by setting the ``slider_icon`` attribute to a drawable resource.

<p align="center">
<img src="assets/custom_icon.png" alt="custom_icon" width="40%"/>
Expand All @@ -231,7 +231,7 @@ sta.setSliderIcon(R.drawable.custom_icon);
You can also disable the rotation by setting the ``rotate_icon`` attribute to false.

#### ``complete_icon``
You can set a custom complete icon by setting the ``complete_icon``attribute to a drawable resource.
You can set a custom complete icon by setting the ``complete_icon`` attribute to a drawable resource.

<p align="center">
<img src="assets/complete_icon.gif" alt="custom_complete_iconcon" width="40%"/>
Expand Down Expand Up @@ -287,13 +287,25 @@ Use the ``android:elevation`` attribute to set the **elevation** of the widget.

<p align="center"><img src="assets/elevation_1.png" alt="elevation_1" width="40%"/> <img src="assets/elevation_2.png" alt="elevation_2" width="40%"/></p>

#### ``slider_loadable``

You can mark a slider as `loadable`. This means that when the slider has started loading (interaction with the slider is complete), your app can perform some work before completing or resetting the slider.

```xml
app:slider_loadable="true"
```

<p align="center">
<img src="assets/loading.gif" alt="loading gif" width="60%"/>
</p>

### Event callbacks

You can use the ``OnSlideCompleteListener`` and the ``OnSlideResetListener`` to simply interact with the widget. If you need to perform operations during animations, you can provide an ``OnSlideToActAnimationEventListener``. With the latter, you will be notified of every animation start/stop.

You can try the **Event Callbacks** in the [Demo app](#demo) to better understand where every callback is called.

<p align="center"><img src="assets/event_log.png" alt="event_log" width="40%"/></p>
<p align="center"><img src="assets/event_log.gif" alt="event_log gif" width="60%" /></p>

## Demo 📲

Expand Down
Binary file added assets/event_log.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/event_log.png
Binary file not shown.
Binary file added assets/loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ protected void onCreate(Bundle savedInstanceState) {
findViewById(R.id.button_reversed_slider).setOnClickListener(this);
findViewById(R.id.button_animation_duration).setOnClickListener(this);
findViewById(R.id.button_bump_vibration).setOnClickListener(this);
findViewById(R.id.button_loadable_slider).setOnClickListener(this);
}

public boolean onCreateOptionsMenu(Menu menu) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ncorti.slidetoact.example;

import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
Expand All @@ -16,6 +17,7 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;


public class SampleActivity extends AppCompatActivity {
Expand Down Expand Up @@ -97,6 +99,36 @@ public void onClick(final View v) {
case R.id.button_bump_vibration:
setContentView(R.layout.content_bumb_vibration);
break;
case R.id.button_loadable_slider:
setContentView(R.layout.content_loadable_slider);
final SlideToActView loadableSliderReset = findViewById(R.id.slide_loadable_reset);
final SlideToActView loadableSliderComplete = findViewById(R.id.slide_loadable_complete);
SlideToActView.OnSlideLoadingStartedListener loadingListener = new SlideToActView.OnSlideLoadingStartedListener() {
@Override
public void onSlideLoadingStarted(final SlideToActView view) {
// Set the text of the slider when it's loading
view.setText(getString(R.string.loading));

Random ran = new Random();
// Simulate an indeterminate amount of time
int delay = ran.nextInt(3000);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (view.isLoadable()) {
if (view.isAnimateCompletion()) {
view.completeSlider();
} else {
view.resetSlider();
}
}
}
}, delay);
}
};
loadableSliderReset.setOnSlideLoadingStartedListener(loadingListener);
loadableSliderComplete.setOnSlideLoadingStartedListener(loadingListener);
break;
default:
finish();
break;
Expand Down Expand Up @@ -140,6 +172,28 @@ public boolean onOptionsItemSelected(MenuItem item) {
private void setupEventCallbacks() {
final SlideToActView slide = findViewById(R.id.event_slider);
final TextView log = findViewById(R.id.event_log);
slide.setOnSlideLoadingStartedListener(new SlideToActView.OnSlideLoadingStartedListener() {
@Override
public void onSlideLoadingStarted(@NonNull final SlideToActView view) {
log.append("\n" + getTime() + " onSlideLoadingStartedListener");
Random ran = new Random();
// Simulate an indeterminate amount of time
final int delay = ran.nextInt(3000);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
log.append("\n" + getTime() + " simulated loading for " + delay + "ms");
if (view.isLoadable()) {
if (view.isAnimateCompletion()) {
view.completeSlider();
} else {
view.resetSlider();
}
}
}
}, delay);
}
});
slide.setOnSlideCompleteListener(new SlideToActView.OnSlideCompleteListener() {
@Override
public void onSlideComplete(@NonNull SlideToActView view) {
Expand Down
6 changes: 6 additions & 0 deletions example/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,11 @@
android:layout_height="wrap_content"
android:text="@string/bump_vibration" />

<Button
android:id="@+id/button_loadable_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/loadable_slider" />

</LinearLayout>
</ScrollView>
1 change: 1 addition & 0 deletions example/src/main/res/layout/content_event_callbacks.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<com.ncorti.slidetoact.SlideToActView
android:id="@+id/event_slider"
style="@style/SlideToActView.Example"
app:slider_loadable="true"
app:text="Test me and read the log" />

<TextView
Expand Down
28 changes: 28 additions & 0 deletions example/src/main/res/layout/content_loadable_slider.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:id="@+id/slide_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical">

<com.ncorti.slidetoact.SlideToActView
android:id="@+id/slide_loadable_reset"
style="@style/SlideToActView.Example"
app:animate_completion="false"
app:slider_loadable="true"
app:text="@string/reset_when_done" />

<com.ncorti.slidetoact.SlideToActView
android:id="@+id/slide_loadable_complete"
style="@style/SlideToActView.Example"
app:slider_loadable="true"
app:text="@string/complete_when_done" />

</LinearLayout>
</ScrollView>
4 changes: 4 additions & 0 deletions example/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@
<string name="use_android_icon">Use Android icon</string>
<string name="use_cloud_icon">Use Cloud icon</string>
<string name="complete_custom_icon">Complete custom icon</string>
<string name="loadable_slider">Loadable Slider</string>
<string name="reset_when_done">Reset when done</string>
<string name="complete_when_done">Complete when done</string>
<string name="loading">Loading…</string>
</resources>
86 changes: 84 additions & 2 deletions slidetoact/src/main/java/com/ncorti/slidetoact/SlideToActView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ class SlideToActView @JvmOverloads constructor(
/** Margin of the cursor from the outer area */
private var mActualAreaMargin: Int
private val mOriginAreaMargin: Int
private var mForceShowText: Boolean = false

var originalText: CharSequence = ""

/** Text message */
var text: CharSequence = ""
set(value) {
field = value
mTextView.text = value
mTextPaint.set(mTextView.paint)
mForceShowText = true
invalidate()
}

Expand Down Expand Up @@ -140,6 +144,9 @@ class SlideToActView @JvmOverloads constructor(
invalidate()
}

/** Private original Slider Icon */
private var originalSliderIcon: Int = R.drawable.slidetoact_ic_arrow

/** Custom Slider Icon */
var sliderIcon: Int = R.drawable.slidetoact_ic_arrow
set(value) {
Expand All @@ -153,6 +160,9 @@ class SlideToActView @JvmOverloads constructor(
}
}

/** Custom Loader Icon */
var loadingIcon: Int = R.drawable.slidetoact_ic_rotate_right

/** Slider cursor position (between 0 and (`mAreaWidth - mAreaHeight)) */
private var mPosition: Int = 0
set(value) {
Expand Down Expand Up @@ -245,6 +255,9 @@ class SlideToActView @JvmOverloads constructor(
/** Private flag to check if the slide gesture have been completed */
private var mIsCompleted = false

/** Public flag to set whether the slider is loadable or not */
var isLoadable = false

/** Public flag to lock the slider */
var isLocked = false

Expand All @@ -266,6 +279,7 @@ class SlideToActView @JvmOverloads constructor(
/** Public Slide event listeners */
var onSlideToActAnimationEventListener: OnSlideToActAnimationEventListener? = null
var onSlideCompleteListener: OnSlideCompleteListener? = null
var onSlideLoadingStartedListener: OnSlideLoadingStartedListener? = null
var onSlideResetListener: OnSlideResetListener? = null
var onSlideUserFailedListener: OnSlideUserFailedListener? = null

Expand Down Expand Up @@ -324,6 +338,7 @@ class SlideToActView @JvmOverloads constructor(
}

text = getString(R.styleable.SlideToActView_text) ?: ""
originalText = text
typeFace = getInt(R.styleable.SlideToActView_text_style, 0)
mTextSize = getDimensionPixelSize(
R.styleable.SlideToActView_text_size,
Expand All @@ -334,6 +349,7 @@ class SlideToActView @JvmOverloads constructor(
// TextAppearance is the last as will have precedence over everything text related.
textAppearance = getResourceId(R.styleable.SlideToActView_text_appearance, 0)

isLoadable = getBoolean(R.styleable.SlideToActView_slider_loadable, false)
isLocked = getBoolean(R.styleable.SlideToActView_slider_locked, false)
isReversed = getBoolean(R.styleable.SlideToActView_slider_reversed, false)
isRotateIcon = getBoolean(R.styleable.SlideToActView_rotate_icon, true)
Expand All @@ -357,6 +373,12 @@ class SlideToActView @JvmOverloads constructor(
R.styleable.SlideToActView_slider_icon, R.drawable.slidetoact_ic_arrow
)

originalSliderIcon = sliderIcon

loadingIcon = getResourceId(
R.styleable.SlideToActView_loading_icon, R.drawable.slidetoact_ic_rotate_right
)

// For icon color. check if the `slide_icon_color` is set.
// if not check if the `outer_color` is set.
// if not, default to defaultOuter.
Expand All @@ -378,6 +400,8 @@ class SlideToActView @JvmOverloads constructor(

mArrowMargin = mIconMargin
mTickMargin = mIconMargin

mForceShowText = false
}
} finally {
attrs.recycle()
Expand Down Expand Up @@ -461,7 +485,7 @@ class SlideToActView @JvmOverloads constructor(
)

// Text alpha
mTextPaint.alpha = (255 * mPositionPercInv).toInt()
mTextPaint.alpha = if (mForceShowText) 255 else (255 * mPositionPercInv).toInt()
// Checking if the TextView has a Transformation method applied (e.g. AllCaps).
val textToDraw = mTextView.transformationMethod?.getTransformation(text, mTextView) ?: text
canvas.drawText(
Expand Down Expand Up @@ -561,7 +585,11 @@ class SlideToActView @JvmOverloads constructor(
positionAnimator.start()
} else if (mPosition > 0 && mPositionPerc >= mGraceValue) {
isEnabled = false // Fully disable touch events
startAnimationComplete()
if (isLoadable) {
startLoading()
} else {
startAnimationComplete()
}
} else if (mFlagMoving && mPosition == 0) {
// mFlagMoving == true means user successfully grabbed the slider,
// but mPosition == 0 means that the slider is released at the beginning
Expand Down Expand Up @@ -633,6 +661,7 @@ class SlideToActView @JvmOverloads constructor(
* Private method that is performed when user completes the slide
*/
private fun startAnimationComplete() {
mForceShowText = false
val animSet = AnimatorSet()

// Animator that moves the cursor
Expand Down Expand Up @@ -710,6 +739,42 @@ class SlideToActView @JvmOverloads constructor(
animSet.start()
}

/**
* Private method that is performed when the slider is loading
*/
private fun startLoading() {
val animSet = AnimatorSet()

if (mPosition < mAreaWidth - mAreaHeight) {
// Animator that moves the cursor
val finalPositionAnimator = ValueAnimator.ofInt(mPosition, mAreaWidth - mAreaHeight)
finalPositionAnimator.addUpdateListener {
mPosition = it.animatedValue as Int
invalidate()
}
animSet.play(finalPositionAnimator)
}
animSet.duration = animDuration

animSet.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
}

override fun onAnimationCancel(p0: Animator?) {
}

override fun onAnimationEnd(p0: Animator?) {
sliderIcon = loadingIcon
mIsCompleted = !isAnimateCompletion
onSlideLoadingStartedListener?.onSlideLoadingStarted(this@SlideToActView)
}

override fun onAnimationRepeat(p0: Animator?) {
}
})
animSet.start()
}

/**
* Method that completes the slider
*/
Expand Down Expand Up @@ -765,6 +830,11 @@ class SlideToActView @JvmOverloads constructor(
val positionAnimator = ValueAnimator.ofInt(mPosition, 0)
positionAnimator.addUpdateListener {
mPosition = it.animatedValue as Int
// When resetting the slider, we need to reset the icon before the handle resets
// Changing the icon here is a timing optimisation
sliderIcon = originalSliderIcon
text = originalText
mForceShowText = false
invalidate()
}

Expand Down Expand Up @@ -908,6 +978,18 @@ class SlideToActView @JvmOverloads constructor(
fun onSlideComplete(view: SlideToActView)
}

/**
* Event handler for the slide loading started event.
* Use this handler to react to slide event
*/
interface OnSlideLoadingStartedListener {
/**
* Called when user performed the slide on a loadable slider
* @param view The SlideToActView who created the event
*/
fun onSlideLoadingStarted(view: SlideToActView)
}

/**
* Event handler for the slide react event.
* Use this handler to inform the user that he can slide again.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
</vector>
Loading