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

Add Ability to Recognize Child View Click #384

Closed
jlwatkins opened this issue Jun 7, 2017 · 13 comments
Closed

Add Ability to Recognize Child View Click #384

jlwatkins opened this issue Jun 7, 2017 · 13 comments

Comments

@jlwatkins
Copy link
Contributor

I would really like to be able to tell whether or not a sub child was click or the parent. Being able to tell by the sub view id would be great. I currently have to change FlexibleAdapter and VIewHolder to get this functionality. I would love it if there was an AdvanedItemClickListener that would pass the sub view's id as well as the position.

@davideas
Copy link
Owner

@jlwatkins, it is an interesting feature, I will try analyse this point for future releases.
Anyone can suggest how to achieve this feature too.

@jlwatkins
Copy link
Contributor Author

What is the easiest way to import this project as a module / library dependency to my project? Do you have any instructions on this? I would love to try to write it.

@davideas
Copy link
Owner

davideas commented Jun 12, 2017

@jlwatkins, if I understood well, you have already a project and want to import the module(s) of this project.
You can try to use the import module in menu, "file/new/import module..." and follow the instructions on screen, but first you need to clone the full FlexibleAdapter project.

If the import doesn't work, you do it manually, copy the module project, copy the parts you need from settings.gradle and adjust the Gradle variables it misses after the manual import. that's it.

@jlwatkins
Copy link
Contributor Author

Yeah to be honest it was a pretty big pain to import it into my project due to Gradle settings. I am not a Gradle expert, but can usually fix most problems. End all be all, it was probably my lack of knowledge with Gradle. I just bailed on this after 2 hours. I ended up extending the FlexibleAdapter and creating a new interface. , but really hate doing it because my code base is so large and changing the original would have been great so "it just works" with all my existing view holders, adapters, and therefore code base.

Side note: I am working on making my code reactive as well (RxJava, RxAndroid, and RxBindings) and would love to make this library reactive as well, but won't do it unless I can edit the library directly in my project. Let me know if you are interested in this or have thought about this at all either.

@davideas
Copy link
Owner

davideas commented Jun 15, 2017

@jlwatkins, so regarding the child view click, what have you done?

For Reactive, I didn't think about it yet, but any core modification lead to a redesign of the library, so for now, it is not the moment for Rx, because another level of abstraction is needed as for the others extensions as well.
All core modifications are postponed after publishing the 5.0 final release.

@davideas
Copy link
Owner

davideas commented Jul 5, 2017

Hi @jlwatkins, did you made some progress for child clicks?

@jlwatkins
Copy link
Contributor Author

I did! It is actually working on our App in production currently. I will attach my code example when available.

@jlwatkins
Copy link
Contributor Author

I had to extend FlexibleAdapter and FlexibleViewHolder in order to implement this function. A couple of concerns/notes/problems I had other than having to extend these classes and having to change my code everywhere were:

  • This functionality will only ever work for views with and @id defined in the layout. This is acceptable and reasonable to me as this id servers as the identifier.
  • Not sure whether it is best to fire the regular onClick if this is captured or not by the new AdvancedItemClickListener. It only fires if the AdvancedItemClickListener's onClick is not returning true which seems pretty standard to me.
  • The name AdvancedItemClickListener should probably be renamed?
  • I find child views automatically or manually. Not sure if I should default to one (automatic). I would probably default to one if I didn't have a large code base and was able to change everything easily in FlexibleAdapter.
  • Not sure if this should work for the main 'ContentView', but I didn't want to have to have both an OnItemClickListener and an AdvancedItemClickListener, so I added the main content view as well.
  • When automatically adding, I am checking if any subview is a ViewGroup and am not sure if any can be missed (ex: I think an Android WebView might have a problem?)
  • Adding manually via addViewToClickListener(View view) is kind of painful and easy to forget when expanding views, but is the main way I use this because I don't want to register a lot of unnecessary listeners

Code coming soon.

@jlwatkins
Copy link
Contributor Author

jlwatkins commented Jul 15, 2017

FlexAdapter:

// imports

public class FlexAdapter<T extends IFlexible> extends FlexibleAdapter<T> {
    public FlexAdapter(@Nullable List<T> items) {
        super(items);
    }

    public FlexAdapter(@Nullable List<T> items, @Nullable Object listeners) {
        super(items, listeners);
    }

    public FlexAdapter(@Nullable List<T> items, @Nullable Object listeners, boolean stableIds) {
        super(items, listeners, stableIds);
    }

    public interface AdvancedItemClickListener {
        boolean onItemClick(int position, View view);
    }

    public AdvancedItemClickListener mAdvancedItemClickListener;

    @Override
    public FlexibleAdapter addListener(@Nullable Object listener) {
        super.addListener(listener);

        if (listener instanceof AdvancedItemClickListener) {
            if (DEBUG) Timber.i("- AdvancedItemClickListener");
            mAdvancedItemClickListener = (AdvancedItemClickListener) listener;
        }

        return this;
    }
}

Simple FlexViewHolder (what I am using):

// imports

public class FlexViewHolder extends FlexibleViewHolder {
    public FlexViewHolder(View view, FlexibleAdapter adapter) {
        super(view, adapter);
    }

    public FlexViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader) {
        super(view, adapter, stickyHeader);
    }


    public void addViewToClickListener(View view) {
        view.setOnClickListener(this);
    }

    @Override
    @CallSuper
    public void onClick(View view) {
        int position = getFlexibleAdapterPosition();
        if (!mAdapter.isEnabled(position)) return;

        if(mAdapter instanceof FlexAdapter) {
            if (((FlexAdapter) mAdapter).mAdvancedItemClickListener != null && mActionState == ItemTouchHelper.ACTION_STATE_IDLE) {
                if (FlexibleAdapter.DEBUG) {
                    Timber.v("onClick on position " + position + " mode=" + mAdapter.getMode());
                }

                if(((FlexAdapter) mAdapter).mAdvancedItemClickListener.onItemClick(position, view)) {
                    toggleActivation();
                }
            }
        } else {
            // Let the normal onClick fire
            super.onClick(view);
        }
    }
}

Complex FlexViewHolder:

// imports

public class FlexViewHolder extends FlexibleViewHolder {
    private static final int UNDEFINED_VIEW_ID = -1;

    public FlexViewHolder(View view, FlexibleAdapter adapter) {
        this(view, adapter, false);
    }

    public FlexViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader) {
        this(view, adapter, stickyHeader, false, null, null);
    }

    public FlexViewHolder(View view, FlexibleAdapter adapter, boolean stickHeader,
                          @Nullable Collection<Integer> includeViewIds,
                          @Nullable Collection<Integer> excludeViewIds) {
        this(view, adapter, stickHeader, true, includeViewIds, excludeViewIds);
    }


    public FlexViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader,
                          boolean registerAllSubviewClicks,
                          @Nullable Collection<Integer> includeViewIds,
                          @Nullable Collection<Integer> excludeViewIds) {
        super(view, adapter, stickyHeader);

        // Validate include and exclude
        if ( !validateIncludeAndExclude(includeViewIds, excludeViewIds)) {
            throw new IllegalArgumentException("Invalid include and exclude, please make sure IDs are different");
        }


        if ( adapter instanceof FlexAdapter) {
            // Make sure no views are declared
            if (viewShouldBeAdded(view, includeViewIds, excludeViewIds)) {
                // Add the parent view regardless of whether it has an ID or not
                addViewToClickListener(view, true);
            }


            if (registerAllSubviewClicks && view instanceof ViewGroup) {
                // Add subviews with Ids
                int viewsAddedCount = recursiveAddSubviewsToClickListener((ViewGroup) view, includeViewIds, excludeViewIds);
                Timber.v("Added %d views to click listener", viewsAddedCount);
            }
        }
    }

    protected boolean validateIncludeAndExclude(@Nullable Collection<Integer> includeViewIds,
                                         @Nullable Collection<Integer> excludeViewIds) {
        if ( includeViewIds == null ) return true;
        if ( excludeViewIds == null ) return true;

        // Invalid to have the same view ID in both
        for (Integer viewId : includeViewIds) {
            if ( excludeViewIds.contains(viewId) ) return false;
        }

        if ( includeViewIds.contains(UNDEFINED_VIEW_ID) ) return false;
        if ( excludeViewIds.contains(UNDEFINED_VIEW_ID) ) return false;

        return true;
    }


    protected boolean viewShouldBeAdded(View view,
                                        @Nullable Collection<Integer> includes,
                                        @Nullable Collection<Integer> excludes) {
        if ( includes != null && excludes != null) {
            return includes.contains(view.getId()) && !excludes.contains(view.getId());
        } else if (includes != null) {
            return includes.contains(view.getId());
        } else if (excludes != null) {
            return !excludes.contains(view.getId());
        } else {
            // If no includes or excludes add view
            return true;
        }
    }

    protected int recursiveAddSubviewsToClickListener(ViewGroup viewGroup,
                                                      @Nullable Collection<Integer> includes,
                                                      @Nullable Collection<Integer> excludes) {
        int viewsAdded = 0;

        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View child = viewGroup.getChildAt(i);
            if ( child instanceof ViewGroup) {
                viewsAdded += recursiveAddSubviewsToClickListener((ViewGroup) child, includes, excludes);
            } else if ( viewShouldBeAdded(child, includes, excludes) ) {
                if ( !child.isClickable() ) child.setClickable(true);
                addViewToClickListener(child);
                viewsAdded += 1;
            }
        }


        if ( viewShouldBeAdded(viewGroup, includes, excludes) ) {
            // If view is defined in includes make sure it is clickable
            if (!viewGroup.isClickable() ) viewGroup.setClickable(true);

            addViewToClickListener(viewGroup, false);
            viewsAdded += 1;
        }

        return viewsAdded;
    }

    public void addViewToClickListener(View view) {
        boolean added = addViewToClickListener(view, false);

        // Throw this error at run time to not allow crazy things to happen
        if ( !added ) {
            throw new IllegalStateException("Could not add view to listener. Please check that it has an @id defined in it's layout");
        }
    }

    protected boolean addViewToClickListener(View view, boolean forceAdd) {
        // Only register views with an ID defined
        if( view.getId() == UNDEFINED_VIEW_ID && !forceAdd ) {
            return false;
        }

        view.setOnClickListener(this);
        return true;
    }

    @Override
    @CallSuper
    public void onClick(View view) {
        int position = getFlexibleAdapterPosition();
        if (!mAdapter.isEnabled(position)) return;

        if(mAdapter instanceof FlexAdapter) {
            FlexAdapter adapter = (FlexAdapter) mAdapter;
            if (adapter.mAdvancedItemClickListener != null && mActionState == ItemTouchHelper.ACTION_STATE_IDLE) {
                if(adapter.mAdvancedItemClickListener.onItemClick(position, view)) {
                    if (FlexibleAdapter.DEBUG) {
                        Timber.v("onClick on position %d mode=%d viewId=%d ", position,  adapter.getMode(), view.getId());
                    }
                    toggleActivation();
                    return;
                }
            }
        }

        // Let the normal onClick fire if it wasn't captured by an AdvancedItemClickListener
        super.onClick(view);

    }
}

@jlwatkins
Copy link
Contributor Author

jlwatkins commented Jul 16, 2017

And just for fun and to help with the constructors and iteration and then again to remove the inheritance problem I wrote this in Kotlin quickly. I was going to try to not subclass FlexibleAdapter and FlexibleViewHolder, but extensions functions weren't enough lol. You can completely ignore this, but thought I would show you as it makes writing libraries a lot easier from what I can tell so far.

See below if interested ;).

Simple Kotlin Rewrite

FlexViewHolder

// imports

private val UNDEFINED_VIEW_ID = -1

open class FlexViewHolder @JvmOverloads constructor(view: View,
                                                    adapter: FlexibleAdapter<*>,
                                                    stickyHeader: Boolean = false,
                                                    registerAllSubviewClicks: Boolean = false,
                                                    includeViewIds: Collection<Int> = emptyList(),
                                                    excludeViewIds: Collection<Int> = emptyList()
) : FlexibleViewHolder(view, adapter, stickyHeader) {

    init {

        if ( registerAllSubviewClicks and (adapter is FlexAdapter<*>)) {
            // Validate include and exclude
            if (!validateIncludeAndExclude(includeViewIds, excludeViewIds)) {
                throw IllegalArgumentException("Invalid include and exclude, please check view IDs")
            }

            if (viewShouldBeAdded(view, includeViewIds, excludeViewIds)) {
                addViewToClickListener(view, true)
            }

            if (view is ViewGroup) {
                val viewsAddedCount = recursiveAddSubviewsToClickListener(view, includeViewIds, excludeViewIds)
                Timber.v("Added $viewsAddedCount views to click listener")
            }
        }
    }

    protected fun validateIncludeAndExclude(includeViewIds: Collection<Int>,
                                            excludeViewIds: Collection<Int>): Boolean {

        val combined = includeViewIds.toSet().union(excludeViewIds.toSet())
        val intersection = includeViewIds.toSet().intersect(excludeViewIds.toSet())

        val allViewsHaveAnId = UNDEFINED_VIEW_ID !in combined
        val viewsAreUnique = intersection.isEmpty()

        return allViewsHaveAnId and viewsAreUnique
    }


    protected fun viewShouldBeAdded(view: View,
                                    includes: Collection<Int>,
                                    excludes: Collection<Int>): Boolean {
        return when {
            includes.isNotEmpty() and excludes.isNotEmpty() -> (view.id in includes) and (view.id !in excludes)
            includes.isNotEmpty() -> view.id in includes
            excludes.isNotEmpty() -> view.id !in excludes
            else -> true
        }
    }

    protected fun recursiveAddSubviewsToClickListener(viewGroup: ViewGroup,
                                                      includes: Collection<Int>,
                                                      excludes: Collection<Int>): Int {
        // Look through all views one level below this view this view and add them
        var viewsAdded = viewGroup.views()
                .filter { viewShouldBeAdded(it, includes, excludes) }
                .onEach { addViewToClickListener(it) }
                .onEach { if (!it.isClickable) it.isClickable = true }
                .size

        // Check for views in ViewGroups
        viewsAdded += viewGroup.children()
                .filter { it is ViewGroup }.map { it as ViewGroup }
                .map { recursiveAddSubviewsToClickListener(it, includes, excludes) }
                .sum()

        return viewsAdded
    }

    fun addViewToClickListener(view: View) {
        val added = addViewToClickListener(view, false)

        // Throw this error at run time to not allow crazy things to happen
        if (!added) {
            throw IllegalStateException("Could not add view to listener. Please check that it has an @id defined in it's layout")
        }
    }

    protected fun addViewToClickListener(view: View, forceAdd: Boolean): Boolean {
        // Only register views with an ID defined
        if (view.id == UNDEFINED_VIEW_ID && forceAdd.not()) {
            return false
        }

        view.setOnClickListener(this)
        return true
    }

    @CallSuper
    override fun onClick(view: View?) {
        val position = flexibleAdapterPosition
        if (!mAdapter.isEnabled(position)) return

        if (mAdapter is FlexAdapter<*> && mActionState == ItemTouchHelper.ACTION_STATE_IDLE) {
            mAdapter.mAdvancedItemClickListener?.let { listener ->
                if ( listener.onItemClick(position, view )) {
                    toggleActivation()
                    return
                }
            }
        }

        // Let the normal onClick fire if it wasn't captured by an AdvancedItemClickListener
        super.onClick(view)
    }
}

ViewGroup Extensions

Required for recursiveAddSubviewsToClickListener function:

// ViewGroup extensions
fun ViewGroup.children() = object : Iterable<View> {
    override fun iterator() = object : Iterator<View> {
        var index = 0
        override fun hasNext(): Boolean = index < childCount
        override fun next(): View = getChildAt(index++)
    }
}

fun ViewGroup.views() = object : Iterable<View> {
    override fun iterator() = object : Iterator<View> {
        var index = 0
        override fun hasNext(): Boolean = index < childCount + 1
        override fun next(): View = if (index++ == 0) this@views else getChildAt(index++)
    }
}

FlexAdapter

// imports

class FlexAdapter<T : IFlexible<*>> @JvmOverloads constructor(
        items: List<T>?,
        listeners: Any? = null,
        stableIds: Boolean = false
) : FlexibleAdapter<T>(items, listeners, stableIds) {

    interface AdvancedItemClickListener {
        fun onItemClick(position: Int, view: View?): Boolean
    }

    var mAdvancedItemClickListener: AdvancedItemClickListener? = null

    override fun addListener(listener: Any?): FlexibleAdapter<*> {
        super.addListener(listener)

        if (listener is AdvancedItemClickListener) {
            if (DEBUG) Timber.i("- AdvancedItemClickListener")
            mAdvancedItemClickListener = listener
        }

        return this
    }
}

@jlwatkins
Copy link
Contributor Author

Forgot to include the actual usage of this. Pretty Simple.

addViewToClickListener in FlexViewHolder

Registering subview R.id.action_button for click inside ViewHolder constructor

Side Note: @BindView annotations from Butterknife is used here instead of findViewById

    class VH extends FlexViewHolder {
        @BindView(R.id.profile_image) ImageView imageView;
        @BindView(R.id.date_text) TextView dateText;
        @BindView(R.id.left_indicator) ImageView leftIndicator;
        @BindView(R.id.right_indicator) ImageView rightIndicator;
        @BindView(R.id.action_button) CircularProgressButton actionButton;
        @BindView(R.id.name_text) TextView name;
        @BindView(R.id.info_text) TextView info;

        public VH(View view, FlexibleAdapter adapter, boolean stickyHeader) {
            super(view, adapter, stickyHeader);
            ButterKnife.bind(this, view);

            if(adapter instanceof FlexAdapter) {
                addViewToClickListener(actionButton);
            }
        }
    }

AdvancedItemClickListener

If R.id.action_button is clicked handleRowUserAction, but if any other thing is clicked then call handleRowUserClick -> (the parent view VH is the only other item that will call this)

Side Note: retrolambda is used here to declare the instance of AdvancedItemClickListener

FlexAdapter.AdvancedItemClickListener discoverClickListener = (position, view) -> {
        Timber.v("Discover Click: " + position);
        RowUser row = discoverAdapter.getItem(position);
        if(view.getId() == R.id.action_button) {
            handleRowUserAction(row);
        } else {
            handleRowUserClick(row);
        }
        return true;
    };

@davideas
Copy link
Owner

@jlwatkins, thanks a lot! 👍 I will review your posts with calm ⏳

@davideas
Copy link
Owner

davideas commented Jan 17, 2018

What if we just add the view to the callback onItemClick(View view, int position) as you also do?
The usage would be only:

actionButton.setClickListener(this);

In the first internal listener I add only the view to the callback and of course, the entire item should be enabled (it is by default):

if (mAdapter.mItemClickListener.onItemClick(view, position)) {
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants