-
Notifications
You must be signed in to change notification settings - Fork 552
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
Comments
@jlwatkins, it is an interesting feature, I will try analyse this point for future releases. |
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. |
@jlwatkins, if I understood well, you have already a project and want to import the module(s) of this 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. |
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. |
@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. |
Hi @jlwatkins, did you made some progress for child clicks? |
I did! It is actually working on our App in production currently. I will attach my code example when available. |
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:
Code coming soon. |
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);
}
} |
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 RewriteFlexViewHolder// 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 ExtensionsRequired for // 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
}
}
|
Forgot to include the actual usage of this. Pretty Simple. addViewToClickListener in FlexViewHolderRegistering subview Side Note: 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);
}
}
} AdvancedItemClickListenerIf Side Note: retrolambda is used here to declare the instance of 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;
}; |
@jlwatkins, thanks a lot! 👍 I will review your posts with calm ⏳ |
What if we just add the view to the callback 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)) {
... |
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.
The text was updated successfully, but these errors were encountered: