1

I am using this github library https://github.com/amlcurran/ShowcaseView to display overlay on view elements of my app during user onboarding (or first time app open after install).

This library requires the view as input and displays overlay on your activity focusing on that view so that you could tell users more about that view.

I have a listView with fast scroll enabled. I want to display overlay on the fast scroll thumb. Hence I want to get view of my listView's fast scroll thumb, but due to absence of any public method in absListView implementation, I'm unable to do this.

Please help.

old_timer
  • 69,149
  • 8
  • 89
  • 168
mumayank
  • 1,670
  • 2
  • 21
  • 34
  • Well, you can do this with reflection, but things vary widely depending on the Android version. Which versions are you supporting? – Mike M. Aug 07 '16 at 06:51
  • Thanks for the comment, @MikeM. I want to support all versions above API 14. Can you guide a bit more, please? – mumayank Aug 07 '16 at 07:31
  • Starting with API 18, the thumb is an `ImageView`, so that's easy enough, but the field name for it is different depending on the specific version. Not a big deal. However, before API 18, the thumb is a `Drawable` that's drawn directly on the `ListView`, so there's no specific `View` that you can pass to the library. I've not used that library, so I don't know if there are other options available. – Mike M. Aug 07 '16 at 07:40
  • Thank you Mike. This helped. I will try for API 18+ for now. – mumayank Aug 07 '16 at 07:44
  • No problem. I just ran a quick test on API 19 that I can post, if you want. It'd take me a few minutes to go through all the source versions to get the field names, though. – Mike M. Aug 07 '16 at 07:47
  • I think I have what you're looking for. I have been trying different ways to get solution for this @MikeM. Here is a gist, please have a look: https://gist.github.com/mumayank/510bd317f46a23d2c7308fd5cad987e3 – mumayank Aug 07 '16 at 07:54
  • Yeah, that's pretty much it. I can post what I have with those values, but it seems like you're already there. What problems were you having, specifically? – Mike M. Aug 07 '16 at 07:57
  • See this line in gist: Object o = f.get(getListView()); While debugging, I found this is throwing an error and the control goes in catch block. The error message is that mThumbView in absListView has private access specifier, hence you cannot access it from outside the class. :( – mumayank Aug 07 '16 at 07:59
  • Oh, OK, no problem. There's a reflection method for that. Btw, I misspoke earlier. The cutoff is API 19, but it looks like you know that already. :-) Gimme a minute, and I'll modify my method. – Mike M. Aug 07 '16 at 08:02
  • Also, I looked inside the showCaseView lib (which is giving me the overlay with view highlighting feature) and found that all this lib needs view for is to locate it. Hence I guess if we can get position for drawables for – mumayank Aug 07 '16 at 08:03
  • Yeah, if you can somehow pass it just the `View`'s coordinates, certainly. I've not had a chance to look at that library yet, though, to see what would need to be changed there. – Mike M. Aug 07 '16 at 08:05

1 Answers1

2

The FastScroller implementation for ListView varies by Android version. Prior to KitKat (API 19), the thumb is a Drawable that's drawn directly on the ListView. Starting with KitKat, the thumb is an ImageView that's added to the ListView's ViewGroupOverlay. In either case, it's easy enough to get what we need through reflection.

Since the ultimate goal is to use this with ShowcaseView, it makes sense to just concern ourselves with the dimensions and coordinates of the thumb, regardless of its specific type. In this way, we can use ShowcaseView's PointTarget, no matter the Android version.

The following reflective method grabs a ListView's FastScroller instance, determines the thumb's size and location using the appropriate type, and returns a Point object with the coordinates of the thumb's center point, if possible.

private Point getFastScrollThumbPoint(final ListView listView) {
    try {
        final Class<?> fastScrollerClass = Class.forName("android.widget.FastScroller");

        final int[] listViewLocation = new int[2];
        listView.getLocationInWindow(listViewLocation);
        int x = listViewLocation[0];
        int y = listViewLocation[1];

        final Field fastScrollerField;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            fastScrollerField = AbsListView.class.getDeclaredField("mFastScroll");
        }
        else {
            fastScrollerField = AbsListView.class.getDeclaredField("mFastScroller");
        }
        fastScrollerField.setAccessible(true);

        final Object fastScroller = fastScrollerField.get(listView);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
            final Field thumbImageViewField = fastScrollerClass.getDeclaredField("mThumbImage");
            thumbImageViewField.setAccessible(true);
            final ImageView thumbImageView = (ImageView) thumbImageViewField.get(fastScroller);

            final int[] thumbViewLocation = new int[2];
            thumbImageView.getLocationInWindow(thumbViewLocation);

            x += thumbViewLocation[0] + thumbImageView.getWidth() / 2;
            y += thumbViewLocation[1] + thumbImageView.getHeight() / 2;
        }
        else {
            final Field thumbDrawableField = fastScrollerClass.getDeclaredField("mThumbDrawable");
            thumbDrawableField.setAccessible(true);
            final Drawable thumbDrawable = (Drawable) thumbDrawableField.get(fastScroller);
            final Rect bounds = thumbDrawable.getBounds();

            final Field thumbYField = fastScrollerClass.getDeclaredField("mThumbY");
            thumbYField.setAccessible(true);
            final int thumbY = (Integer) thumbYField.get(fastScroller);

            x += bounds.left + bounds.width() / 2;
            y += thumbY + bounds.height() / 2;
        }

        return new Point(x, y);
    }
    catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}

To use this with ShowcaseView, we simply check that the returned Point is not null, and pass the Builder a PointTarget created from the return.

Point thumbPoint = getFastScrollThumbPoint(listView);

if (thumbPoint != null) {
    new ShowcaseView.Builder(this)
        .setTarget(new PointTarget(thumbPoint))
        .setContentTitle("ShowcaseView")
        .setContentText("This is highlighting the fast scroll thumb")
        .hideOnTouchOutside()
        .build();
}
Mike M.
  • 38,532
  • 8
  • 99
  • 95
  • Thank you for this answer, @MikeM. Let me quickly run this on API 19+ and mark this answer has accepted. :) – mumayank Aug 07 '16 at 08:16
  • No problem. For a simple test, I just set a different image on the `ImageView` to make sure I was getting the right thing. When I get time later on, probably not until tomorrow, I can take a look at that library, and see what we can do with a `Drawable` for the earlier versions. – Mike M. Aug 07 '16 at 08:18
  • You're just great, @MikeM. Thanks a lot! This is going to save me a lot of time + I learnt new way of getting views in android :) I'd also look at getting `Drawable` issue. If I couldn't get any solution, I'd be grateful to you if you could guide further when you get time next. Nonetheless, this definitely was helpful! – mumayank Aug 07 '16 at 08:21
  • Did it work for ya? Yeah, I'll definitely have a look at the `ShowcaseView`, when I get a chance. It seems to be a popular library, so it'd be handy for me to get familiar with it, anyway. – Mike M. Aug 07 '16 at 08:26
  • I just had a quick look at that, and it seems like you would be able to use a `PointTarget` for the versions with a `Drawable` thumb. The reflection stuff will be a little different, but it definitely seems doable. – Mike M. Aug 07 '16 at 08:38
  • this at least worked in a way that it is not a roadblock anymore. The `PointTarget` for `Thumb View` seems to be pointed at top-right of the screen (weird!). But this answer definitely has set the ball rolling. When you get time, please add code for Drawable too. I'd appreciate if you could look into the lib and find how to properly implement list view thumb highlighting overlay. Thank you :) – mumayank Aug 07 '16 at 19:18
  • OK, I think I've got it. Unfortunately, I'm unable to test anything but KitKat, at the moment. My dev machine is packed away for moving. I'll post what I've got, but I'll need you to do the final testing for all the versions, if you could, please. Just lemme know if something's wrong. Thanks! – Mike M. Aug 07 '16 at 22:05
  • Alrighty, I updated the code. My main concern is with the vertical positioning in API 18 and below, since I can't test that atm. Please let me know what happens. – Mike M. Aug 07 '16 at 23:04
  • Thank you so much! I got 2 errors. First, catch error (I have pushed an edit for the same in your answer). Second, consider this line of code in your answer for – mumayank Aug 08 '16 at 02:09
  • Yeah, I caught only the necessary checked Exceptions for debugging purposes; so it would crash on whatever. I was gonna do a broad catch after you got a chance to check it out, but you obviously know what you're looking for, so I went ahead and approved your edit. I'm in the middle of something right this minute, but I'll have a look at that as soon as I can. – Mike M. Aug 08 '16 at 02:18
  • No issues, @MikeM. You have taught me so much via this answer of yours and our discussion. I am at it, if I couldn't find a solution, I hope you'd get free soon and edit the answer :) – mumayank Aug 08 '16 at 02:23
  • OK, I'm back. Yeah, I forgot you can't have multiple `Exception`s in a `catch` on older versions. Just make sure to check your logs if things don't work as you expect, 'cause catching `Exception` will swallow everything. You might also put a `Toast` or something there, to make it obvious if something fails. Anyhoo, just cast that `get()` return to `Integer`. That is, `final int thumbY = (Integer) thumbYField.get(fastScroller);`. – Mike M. Aug 08 '16 at 03:01
  • Hey, did you get a chance to test this on API < 19? I was curious if the vertical positioning was correct. Please lemme know, if you get a second. Thanks! – Mike M. Aug 10 '16 at 14:14
  • Hi @MikiM., Yes, what a timing you have! I was just about to update. This is a perfect solution! Really thanks for saving the day! The only change I needed to do is to do a little bit of hack. My listView content are fetched from network and hence listView items are set with a delay. Hence when app runs this method, there is no fast scroll/ thumb and returns x=0 (but y sets properly). So I hacked it to always say x= device width :) Works seamlessly! – mumayank Aug 10 '16 at 14:17
  • Ah, yeah, that makes sense. I was doing my tests on a pre-populated `ListView` with the fast scroll always visible. That's excellent. Thanks for the feedback! Cheers! – Mike M. Aug 10 '16 at 14:24
  • Great to know your passion and approach towards problem solving Mike. And yes, I almost forgot to update you: Your solution works on API =KITKAT, both :) – mumayank Aug 10 '16 at 14:26