仅使用栏而不是子视图本身滚动

问题描述

在我的应用程序中,我有一个如下图所示的自定义视图,

Audio Visualizer

这是一个音频可视化器,它在放大时跨 X 轴增长。所以我需要在滚动视图中使用它来滚动音频图。布局目前完成如下,

<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>

自定义视图 FileWaveView 是从 LinearLayout 扩展而来的。

现在我想要一个滚动行为,这样当我在自定义视图(音频可视化图形)内滚动时,滚动视图不应该滚动。但是当我通过触摸栏本身滚动时,滚动条会滚动它的内容

我想要这种行为,因为我希望滚动手势用于可视化图表内的其他一些操作(调整段选择器宽度,我稍后会集成)。

请建议我如何制作此滚动条滚动模式,而视图将仅在滚动条中滚动。我应该避免使用 Horizo​​ntalScrollBar 并在自定义视图本身内部创建自己的滚动行为吗?

解决方法

在 SlothCoding 的帮助下,我解决了这个问题。我创建了一个自定义滚动条并用它来控制 Horizo​​ntalScrollView。相关代码如下,

自定义滚动视图

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
    }
}

滚动条

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
                    }
                }
            }
        }
    }
}

滚动条布局,

<?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>

滚动条内部自定义视图的布局,

<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" />

最后在父视图中,

binding.apply {
    fileWaveViewScrollBar.setHorizontalScrollView(fileWaveViewScroll)
    fileWaveViewScrollBar.setControlledView(fileWaveView)
}

我们有一些用于滚动轨道和滚动拇指的可绘制对象。

滚动轨迹,

<?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>

滚动拇指,

<?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>

,

TL;DR

没有任何东西可以帮助您通过滑动事件禁用 Horizo​​ntalScrollView 上的滚动,并且仅在使用 ScrollBar 时才这样做。这样做的原因是 ScrollBar 不是与 Horizo​​ntalScrollView 分开的视图,并且它本身没有实现任何侦听器。第三方库可以选择处理音频可视化或音频文件,或者实施我在下面发布的这种解决方法。一开始不太确定它是否会起作用,因为它需要大量的工作,但也许值得一试。


我不确定 HorizontalScrollView 方法中是否有类似的东西。但是有一些选项可以帮助您做到这一点。我找不到任何东西(例如教程)来为您实现这一点,但我可能会通过一些变通方法来完成这项工作。首先,有这个:

您不能禁用 ScrollView 的滚动。您需要扩展到 ScrollView 并覆盖 onTouchEvent 方法以在匹配某些条件时返回 false。

因此,您需要首先执行此操作以防止用户滚动视图。这将禁用触摸滚动,这就是您在这种情况下需要的。现在,我不确定这是否会 100% 奏效,因为我现在无法对其进行测试,但我正在尽力提供帮助。在您禁用 ScrollView 上的触摸事件后,现在您需要隐藏 ScrollBar,因为您无法使用它,因为它是 HorizontalScrollView 的一部分并且它不仅具有他自己的听众所以我认为每个触摸事件都会被 HorizontalScrollView 本身拦截,而不是在内容和滚动条上分开。要隐藏 HorizontalScrollView 中的滚动条,您可以使用 XML 属性,如下所示:

android:scrollbars="none"

或者像这样从你的代码中:

view.setVerticalScrollBarEnabled(false);
view.setHorizontalScrollBarEnabled(false);

现在您需要创建自己的滚动条或仅用于在视图内滚动的按钮。这些按钮将更改滚动位置 onClick 事件。但首先,让我们禁用 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;
    }

}

现在您可以使用这个代替经典的 HorizontalScrollView

<com.mypackagename.LockableScrollView 
    android:id="@+id/QuranGalleryScrollView" 
    android:layout_height="fill_parent" 
    android:layout_width="fill_parent">

    //your view

</com.mypackagename.LockableScrollView>

这是来自这个问题的答案:https://stackoverflow.com/a/5763815/14759470

现在,让我们回到那些按钮。假设您在每一侧都有两个按钮,一个用于向右滚动,一个用于向左滚动。您可以像这样处理滚动:

rightBtn.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        hsv.scrollTo((int)hsv.getScrollX() + 10,(int)hsv.getScrollY());
    }
});

或者,如果您希望它流畅,您可以像这样使用 onTouchListener

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);
        }
    };
});

其中 hsv 是您的 HorizontalScrollView。现在您可以遵循左按钮的逻辑。要在视图开始/结束时处理显示/隐藏按钮,您可以按照以下问题和答案进行操作:https://stackoverflow.com/a/7672711/14759470

如果这不是您想做的事情,那么我不确定您还有其他选择,因为我的知识只能做到这一点。但是,也许可以考虑在 GitHub 或其他地方查看一些处理音频文件的第三方库。像这样:https://github.com/ferPrieto/SoundLine

如果您在实施此代码时遇到任何问题,请在评论中告诉我。由于我在发布答案之前没有时间对此进行测试,但我很乐意帮助您解决未来遇到的任何问题。