0

I am attempting to create a custom, vertical SeekBar that will be used to scroll text within a ScrollView. Here is that SeekBar:

class CustomSeekBar : SeekBar {

constructor(context: Context) : super(context) {}

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(h, w, oldh, oldw)
}

@Synchronized
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(heightMeasureSpec, widthMeasureSpec)
    setMeasuredDimension(measuredHeight, measuredWidth)
}

override fun onDraw(c: Canvas) {

    c.rotate(90f)
    c.translate(0f, -width.toFloat())

    super.onDraw(c)
}

override fun onTouchEvent(event: MotionEvent): Boolean {

    super.onTouchEvent(event)
    if (!isEnabled) {
        return false
    }

    when (event.action) {
        MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {

            val i = (max - (max * event.y / height)).toInt()
            progress = 100 - i
            onSizeChanged(width, height, 0, 0)
        }

        MotionEvent.ACTION_CANCEL -> {
        }
    }
    return true
}
}

Here is my code that attempts to attach the ScrollView to the SeekBar in the onStart of my Fragment:

       override fun onStart() {
    super.onStart()

    val helpScrollView = view!!.findViewById<ScrollView>(R.id.helpScrollView)

    val helpSeekBar = view!!.findViewById<CustomSeekBar>(R.id.helpSeekBar)

   helpScrollView.viewTreeObserver.addOnGlobalLayoutListener {

       val scroll: Int = getScrollRange(helpScrollView)

       helpSeekBar.max = scroll
   }

    val seekBarListener = object : SeekBar.OnSeekBarChangeListener {

        override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {

            val min = 0
            if(progress < min) {
                seekBar?.progress = min;}

            helpScrollView.scrollTo(0, progress)

        }
        override fun onStartTrackingTouch(seekBar: SeekBar?) {
        }
        override fun onStopTrackingTouch(seekBar: SeekBar?) {

        }
    }

    helpSeekBar.setOnSeekBarChangeListener(seekBarListener)

}

private fun getScrollRange(scrollView: ScrollView): Int {

    var scrollRange = 0
    if (scrollView.childCount > 0) {
        val child = scrollView.getChildAt(0)
        scrollRange =
            Math.max(0, child.height - (scrollView.height - scrollView.paddingBottom - scrollView.paddingTop))
    }

    return scrollRange
}

The SeekBar on seems to only react to touches on its lower half, and the thumbs shadow still scrolls horizontally off the screen. The ScrollView moves slightly, but only in one direction at the beginning of the scroll. What am I doing wrong?

andrewedgar
  • 797
  • 1
  • 12
  • 46

1 Answers1

3

Check updated answer at the bottom for a solution


Who are we to judge what you need to do, client is almost always right!

Anyway, if you still wanted to do this, (it may or may not work) here's what I think the architecture should try to look like:

  1. The SeekBar is stupid, shouldn't know ANYTHING about what it is doing. You give it a min (0?) and a MAX (n). You subscribe to its listener and receive the "progress" updates. (ha, that's the easy part)
  2. ScrollView containing the text, is also very unintelligent beyond its basics, it can be told to scroll to a position (this will involve some work I guess, but I'm sure it's not difficult).
  3. Calculating the MAX value is going to be what determines how hard step 2 will be. If you use each "step" in your scrolling content as a "Line of text", then this is fine, but you'd need to account for layout changes and re-measures that may change the size of the text. Shouldn't be "crazy" but, keep it in mind or you will receive your first bug report as soon as a user rotates the phone :)
  4. If the text is dynamic (can change real-time) your function for step 3 should be as efficient as possible, you want those numbers "asap".
  5. Responding to progress changes from the seekbar and having your scrollable content scroll is going to be either super easy (you found a way to do it very easily) or complicated if you cannot make the ScrollView behave as you want.

Alternative Approach

If your text is really long, I bet you're going to have better performance if you split your text in lines and use a recyclerview to display it, which would then make scrolling "slightly easier" as you could move your recyclerview to a position (line!).

UPDATE

Ok, so out of curiosity, I tried this. I launched AS, created a new project with an "Empty Activity" and added a ScrollView with a TextView inside, and a SeekBar at the bottom (horizontal).

Here's the Gist with all the relevant bits:

https://gist.github.com/Gryzor/5a68e4d247f4db1e0d1d77c40576af33

At its core the solution works out of the box:

scrollView.scrollTo(0, progress) where progress is returned to you by the framework callback in the seekBar's onProgressChanged().

Now the key is to set the correct "Max" to the seekbar.

This is obtained by dodging a private method in the ScrollView with this:

(credit where is due, I got this idea from here)

    private fun getScrollRange(scrollView: ScrollView): Int {

        var scrollRange = 0
        if (scrollView.childCount > 0) {
            val child = scrollView.getChildAt(0)
            scrollRange =
                Math.max(0, child.height - (scrollView.height - scrollView.paddingBottom - scrollView.paddingTop))
        }

        return scrollRange
    }

This works fine, assuming you wait for the seekBar to measure/layout (hence why I had to do this in the treeObserver).

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • Thank you for your response. We're of a similar mind about architecture. I set the max of the SeekBar to the maxScrollAmount of the ScrollView. I've yet to even get to the point where I can even see if this has worked, though, as I don't believe the SeekBar is even being drawn properly yet. For what it's worth, this app is being embedded on a machine with a single screen size and no configuration changes – andrewedgar Jun 10 '19 at 15:56
  • What are you using to "scroll"? A `ScrollView` with a `TextView` inside? – Martin Marconcini Jun 10 '19 at 16:26
  • Yes, a textview within a scroll view was recommended elsewhere. – andrewedgar Jun 10 '19 at 16:33
  • out of curiosity, what values is the "scrollview" reporting as "maxScrollAmount"? Are you sure this is the correct value to use? (I know you haven't gotten there yet) – Martin Marconcini Jun 10 '19 at 16:40
  • 335 is the max scroll amount. It remains as such even when I make all of the text appear within the ScrollView, so I suspect that it is not the right value to use, despite me finding it in this example (https://stackoverflow.com/questions/50927190/how-to-connect-seekbar-and-scrollview), which the author claims works, though I do not know where he gets his "progress_value" value from. – andrewedgar Jun 10 '19 at 17:31
  • Ok, I got it working, check out the updated answer ;) – Martin Marconcini Jun 10 '19 at 17:41
  • 1
    I'm marking this answer as correct, as I now realize that my code has multiple flaws, and you definitely solved one of them. Thank you so much. If you care to stick around, I have uploaded a video about displaying the the SeekBar's other bit of bad behavior (https://www.youtube.com/watch?v=1XGPXF28ONg&feature=youtu.be) – andrewedgar Jun 10 '19 at 18:06
  • Ha! Thanks and I'm glad you found flaws in your code, I *only* write flaws. :) Anyway, thanks for sharing the video, strange behavior indeed! Tag me again if you have want me to look into something else. Good luck! :) – Martin Marconcini Jun 10 '19 at 18:15
  • Just that bit in the video, if next time you find yourself with a moment. I suspect it has something to do with the custom SeekBar class, but I'm not sure what. I've been fiddling with the values in there but changing anything usually makes the entire seekbar disappear. I think I'll ask another question on SO – andrewedgar Jun 10 '19 at 18:17
  • Ok, mine works better: https://youtu.be/pvk4EKbnAe4 ;) Regarding your seekbar, in all honestly, I'm not sure what's going on w/out seeing a bigger picture. SeekBars are weird components :p i'll take a second look. – Martin Marconcini Jun 10 '19 at 18:19
  • I noticed in your Custom Seekbar... you override onMeasure (no need for that @Synchronized thing, that's all UI thread), but then you call `setMeasuredDimension(measuredHeight, measuredWidth)` I think those are inverted, the signature for the method is `protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {` is this intentional? – Martin Marconcini Jun 10 '19 at 18:46
  • While I copied this from another post that claimed to do what I am trying to, I do believe that the inversion is intentional, as it is part of making the SeekBar vertical insteat of horizontal. Reversing it turns the Seekbar into a small stub, albeit one that still scrolls vertically – andrewedgar Jun 10 '19 at 18:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/194717/discussion-between-martin-marconcini-and-aedgar777). – Martin Marconcini Jun 10 '19 at 20:17