Reputation: 53
In my app I have a custom view like below image,
This is an audio visualiser, which grow across X axis upon zoom in. So I need it inside a scrollview to scroll through the audio graph. The layout is currently done like below,
<HorizontalScrollView
android:id="@+id/fileWaveViewScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
style="@style/CustomScrollbar">
<com.bluehub.fastmixer.common.views.FileWaveView
android:id="@+id/fileWaveView"
android:layout_width="wrap_content"
android:layout_height="120dp"
app:audioFileUiState="@{audioFileUiState}"
app:samplesReader="@{eventListener.readSamplesCallbackWithIndex(audioFileUiState.path)}"
app:fileWaveViewStore="@{fileWaveViewStore}"
tools:layout_height="120dp"/>
</HorizontalScrollView>
Custom view FileWaveView
is extended from a LinearLayout.
Now I want a the scrolling behaviour such that when I am scrolling inside the custom view (audio visualiser graph), the scrollview should not scroll. But when I am scrolling by touching the bar itself, then the scrollbar will scroll it's content.
I want this behaviour because I want the scroll gesture to be used for some other action inside the visualiser graph (to adjust a segment selector width, which I will integrate later).
Please suggest me how can I make this scrollbar scrolling pattern, while the view will be scrolled through the bar only. Should I refrain from using HorizontalScrollBar and make own scrolling behaviour inside of the custom view itself?
Upvotes: 3
Views: 471
Reputation: 53
From kind help of SlothCoding, I was able to fix the issue. I created a custom scrollbar and used it to control the HorizontalScrollView. The relevant code is given below,
Custom Scroll View
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.HorizontalScrollView
class LockedHorizontalScrollView(
context: Context,
attrs: AttributeSet? = null) : HorizontalScrollView(context, attrs) {
init {
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
return false
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return false
}
}
ScrollBar
package com.bluehub.fastmixer.common.views
import android.content.Context
import android.util.AttributeSet
import android.view.*
import android.widget.HorizontalScrollView
import androidx.constraintlayout.widget.ConstraintLayout
import com.bluehub.fastmixer.databinding.CustomScrollBarBinding
class CustomHorizontalScrollBar(context: Context, attributeSet: AttributeSet?)
: ConstraintLayout(context, attributeSet) {
private val binding: CustomScrollBarBinding
private lateinit var mHorizontalScrollView: HorizontalScrollView
private lateinit var mView: View
private val mScrollBar: CustomHorizontalScrollBar
private val mScrollTrack: View
private val mScrollThumb: View
private val gestureListener = object: GestureDetector.SimpleOnGestureListener() {
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
handleScrollOfThumb(e2, distanceX)
return true
}
override fun onDown(e: MotionEvent?): Boolean {
return true
}
}
private val gestureDetector: GestureDetector
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
binding = CustomScrollBarBinding.inflate(inflater, this, true)
mScrollBar = this
mScrollTrack = binding.scrollTrack
mScrollThumb = binding.scrollThumb
gestureDetector = GestureDetector(context, gestureListener)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event)
}
fun setHorizontalScrollView(horizontalScrollView: HorizontalScrollView) {
this.mHorizontalScrollView = horizontalScrollView
}
fun setControlledView(view: View) {
this.mView = view
setViewWidthListener()
}
private fun handleScrollOfThumb(event: MotionEvent?, distanceX: Float) {
event?: return
if (event.x >= mScrollThumb.left && event.x <= mScrollThumb.left + mScrollThumb.width) {
var newLeft = mScrollThumb.left - distanceX.toInt()
var newRight = mScrollThumb.right - distanceX.toInt()
if (newRight <= mScrollBar.width && newLeft >= 0) {
repositionScrollThumb(newLeft, newRight)
}
// Thumb is not at left nor right, but distanced asked to traverse by move is more
else if (mScrollThumb.left != 0 && mScrollThumb.right < mScrollBar.width - 1) {
// Distance from right end of bar
val distanceToRight = mScrollBar.width - mScrollThumb.right - 1
// Get the closest distance to move the thumb to
val newDist = if (mScrollThumb.left < distanceToRight) {
mScrollThumb.left
} else {
-distanceToRight
}
newLeft = mScrollThumb.left - newDist
newRight = mScrollThumb.right - newDist
repositionScrollThumb(newLeft, newRight)
}
}
}
private fun repositionScrollThumb(newLeft: Int, newRight: Int) {
mScrollThumb.layout(newLeft, mScrollThumb.top, newRight, mScrollThumb.bottom)
performScrollOnScrollView()
}
private fun performScrollOnScrollView() {
val ratio = mView.width.toFloat() / mScrollBar.width.toFloat()
val posToScroll = (ratio * mScrollThumb.left).toInt()
mHorizontalScrollView.post {
mHorizontalScrollView.scrollTo(posToScroll, mHorizontalScrollView.top)
}
}
private fun setViewWidthListener() {
mView.addOnLayoutChangeListener { view: View, _, _, _, _, _, _, _, _ ->
if (mScrollTrack.width == 0) return@addOnLayoutChangeListener
val w = view.width
val ratio = w.toFloat() / mScrollTrack.width.toFloat()
if (ratio == 1.0f) {
mScrollBar.visibility = View.INVISIBLE
} else {
mScrollBar.visibility = View.VISIBLE
val layoutParams = mScrollThumb.layoutParams
val newWidth = (mScrollTrack.width.toFloat() / ratio).toInt()
if (mScrollThumb.width != newWidth) {
mScrollThumb.post {
mScrollThumb.layoutParams = layoutParams
}
}
}
}
}
}
Scrollbar Layout,
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="1dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5"/>
<View
android:id="@+id/scrollTrack"
android:layout_width="match_parent"
android:layout_height="@dimen/scrollbar_track_height"
android:background="@drawable/scrollbar_horizontal_track"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/scrollThumb"
android:layout_width="18dp"
android:layout_height="@dimen/scrollbar_thumb_height"
android:background="@drawable/scrollbar_horizontal_thumb"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Layout of the custom view inside of the scrollbar,
<com.bluehub.fastmixer.common.views.LockedHorizontalScrollView
android:id="@+id/fileWaveViewScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.bluehub.fastmixer.common.views.FileWaveView
android:id="@+id/fileWaveView"
android:layout_width="wrap_content"
android:layout_height="@dimen/audio_file_view_height"
app:audioFileUiState="@{audioFileUiState}"
app:samplesReader="@{eventListener.readSamplesCallbackWithIndex(audioFileUiState.path)}"
app:fileWaveViewStore="@{fileWaveViewStore}"
tools:layout_height="120dp">
<com.bluehub.fastmixer.common.views.AudioWidgetSlider
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</com.bluehub.fastmixer.common.views.FileWaveView>
</com.bluehub.fastmixer.common.views.LockableHorizontalScrollView>
<com.bluehub.fastmixer.common.views.CustomHorizontalScrollBar
android:id="@+id/fileWaveViewScrollBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fileWaveViewScroll" />
Finally in the parent view,
binding.apply {
fileWaveViewScrollBar.setHorizontalScrollView(fileWaveViewScroll)
fileWaveViewScrollBar.setControlledView(fileWaveView)
}
We have some drawables for scroll track and scroll thumb.
Scroll track,
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<gradient
android:angle="0"
android:endColor="#9BA3C5"
android:startColor="#8388A4" />
<corners android:radius="8dp" />
</shape>
Scroll thumb,
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<gradient
android:angle="0"
android:endColor="#005A87"
android:startColor="#007AB8" />
<corners android:radius="8dp" />
</shape>
Upvotes: 2
Reputation: 1706
There isn't anything that would help you disable your scrolling on HorizontalScrollView with slide event and only do it when ScrollBar
is used. The reason for this is that ScrollBar isn't a separate view from HorizontalScrollView and it doesn't have any listeners implemented on itself. There is an option for third-party libraries to work with audio visualization or audio files or to implement this workaround I posted below. Not quite sure if it is going to work at first because it needs a lot of work but maybe is worth trying.
I am not sure that there is something like that in HorizontalScrollView
methods. But there are some options which can help you do this. I couldn't find anything, like a tutorial, to make this happen for you but I might make this work with some workarounds. First, there is this:
You cannot disable the scrolling of a
ScrollView
. You would need to extend to ScrollView and override theonTouchEvent
method to return false when some condition is matched.
So, you need to first do this in order to prevent the user from scrolling the view. This will disable scrolling on touch and that's what you need in this case. Now, I am not sure if this will work 100% since I can't test it right now but I am doing my best to help. After you disabled your touch events on your ScrollView
now you need to hide the ScrollBar
, because you can't use it since it is part of the HorizontalScrollView
and it doesn't have only listeners of himself so I think each touch event will be intercepted by the HorizontalScrollView
itself and not separated on content and scrollbar. To hide the scrollbar in your HorizontalScrollView
you can use XML
attribute like this:
android:scrollbars="none"
or from your code like this:
view.setVerticalScrollBarEnabled(false);
view.setHorizontalScrollBarEnabled(false);
Now you need to create your own scrollbar or just buttons that will do the scrolling inside the view. These buttons will change the scrolling position onClick
event. But first, let's disable the scrolling on HorizontalScrollView
.
class LockableScrollView extends HorizontalScrollView {
...
// true if we can scroll (not locked)
// false if we cannot scroll (locked)
private boolean mScrollable = true;
public void setScrollingEnabled(boolean enabled) {
mScrollable = enabled;
}
public boolean isScrollable() {
return mScrollable;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Do not allow touch events.
return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// Do not allow touch events.
return false;
}
}
Now instead of classic HorizontalScrollView
you can use this:
<com.mypackagename.LockableScrollView
android:id="@+id/QuranGalleryScrollView"
android:layout_height="fill_parent"
android:layout_width="fill_parent">
//your view
</com.mypackagename.LockableScrollView>
This is from the answer to this question: https://stackoverflow.com/a/5763815/14759470
Now, let's go back to those buttons. Let's say you have two buttons on each side, one for scroll to right and one for scroll to left. You can handle scrolling like this:
rightBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
hsv.scrollTo((int)hsv.getScrollX() + 10, (int)hsv.getScrollY());
}
});
or if you want it be smooth you can use onTouchListener
like this:
rightBtn.setOnTouchListener(new View.OnTouchListener() {
private Handler mHandler;
private long mInitialDelay = 300;
private long mRepeatDelay = 100;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mHandler != null)
return true;
mHandler = new Handler();
mHandler.postDelayed(mAction, mInitialDelay);
break;
case MotionEvent.ACTION_UP:
if (mHandler == null)
return true;
mHandler.removeCallbacks(mAction);
mHandler = null;
break;
}
return false;
}
Runnable mAction = new Runnable() {
@Override
public void run() {
hsv.scrollTo((int) hsv.getScrollX() + 10, (int) hsv.getScrollY());
mHandler.postDelayed(mAction, mRepeatDelay);
}
};
});
where hsv
is your HorizontalScrollView
. Now you can follow the logic for the left button. To handle show/hide buttons when the view is at the start/end you can follow this question and answers: https://stackoverflow.com/a/7672711/14759470
If this isn't something you would like to do then I am not sure of other options you have since my knowledge can only go this far. But, maybe consider looking at some third-party libraries on GitHub or somewhere else which are working with audio files. Like this one: https://github.com/ferPrieto/SoundLine
If you run into any problems while implementing this code please let me know in the comments. Since I didn't have time to test this before posting as an answer, but I'll be glad to help you with any future issues you come across.
Upvotes: 1