From ea0ead0ddfefc303c6c79f2aef12e427c8343e2d Mon Sep 17 00:00:00 2001 From: marcel Date: Thu, 7 May 2015 19:25:22 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + README.md | 1 - build.gradle | 19 ++ example/build.gradle | 27 ++ example/proguard-rules.pro | 17 + example/src/main/AndroidManifest.xml | 21 ++ .../java/rxbonjour/example/BonjourVH.java | 37 +++ .../java/rxbonjour/example/MainActivity.java | 116 +++++++ .../main/java/rxbonjour/example/rv/Rv.java | 151 +++++++++ .../rxbonjour/example/rv/RvBaseAdapter.java | 293 ++++++++++++++++++ .../rxbonjour/example/rv/RvBaseHolder.java | 132 ++++++++ example/src/main/res/layout/activity_main.xml | 47 +++ .../main/res/layout/item_bonjourservice.xml | 27 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes example/src/main/res/values/strings.xml | 9 + example/src/main/res/values/styles.xml | 15 + gradle.properties | 16 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++++++++++ gradlew.bat | 90 ++++++ lib/build.gradle | 21 ++ lib/proguard-rules.pro | 17 + lib/src/main/AndroidManifest.xml | 2 + lib/src/main/java/rxbonjour/RxBonjour.java | 67 ++++ .../java/rxbonjour/exc/DiscoveryFailed.java | 13 + .../rxbonjour/exc/TypeMalformedException.java | 11 + .../main/java/rxbonjour/internal/Backlog.java | 87 ++++++ .../rxbonjour/internal/BonjourDiscovery.java | 19 ++ .../internal/JBBonjourDiscovery.java | 143 +++++++++ .../internal/SupportBonjourDiscovery.java | 148 +++++++++ .../java/rxbonjour/model/BonjourEvent.java | 52 ++++ .../java/rxbonjour/model/BonjourService.java | 63 ++++ settings.gradle | 2 + 37 files changed, 1839 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 example/build.gradle create mode 100644 example/proguard-rules.pro create mode 100644 example/src/main/AndroidManifest.xml create mode 100644 example/src/main/java/rxbonjour/example/BonjourVH.java create mode 100644 example/src/main/java/rxbonjour/example/MainActivity.java create mode 100644 example/src/main/java/rxbonjour/example/rv/Rv.java create mode 100644 example/src/main/java/rxbonjour/example/rv/RvBaseAdapter.java create mode 100644 example/src/main/java/rxbonjour/example/rv/RvBaseHolder.java create mode 100644 example/src/main/res/layout/activity_main.xml create mode 100644 example/src/main/res/layout/item_bonjourservice.xml create mode 100644 example/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/src/main/res/values/strings.xml create mode 100644 example/src/main/res/values/styles.xml create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 lib/build.gradle create mode 100644 lib/proguard-rules.pro create mode 100644 lib/src/main/AndroidManifest.xml create mode 100644 lib/src/main/java/rxbonjour/RxBonjour.java create mode 100644 lib/src/main/java/rxbonjour/exc/DiscoveryFailed.java create mode 100644 lib/src/main/java/rxbonjour/exc/TypeMalformedException.java create mode 100644 lib/src/main/java/rxbonjour/internal/Backlog.java create mode 100644 lib/src/main/java/rxbonjour/internal/BonjourDiscovery.java create mode 100644 lib/src/main/java/rxbonjour/internal/JBBonjourDiscovery.java create mode 100644 lib/src/main/java/rxbonjour/internal/SupportBonjourDiscovery.java create mode 100644 lib/src/main/java/rxbonjour/model/BonjourEvent.java create mode 100644 lib/src/main/java/rxbonjour/model/BonjourService.java create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e479603 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +*.iml \ No newline at end of file diff --git a/README.md b/README.md index 967bf51..739faf2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ Start a network service discovery using `RxBonjour.startDiscovery(Context, Strin RxBonjour.startDiscovery(this, "_http._tcp") .subscribe(new Action1() { @Override public void call(BonjourEvent bonjourEvent) { - // Depending on the type of event and the availability of the item, adjust the adapter BonjourService item = bonjourEvent.getService(); switch (bonjourEvent.getType()) { case ADDED: diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..43fc425 --- /dev/null +++ b/build.gradle @@ -0,0 +1,19 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.2.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/example/build.gradle b/example/build.gradle new file mode 100644 index 0000000..bc16ef1 --- /dev/null +++ b/example/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion COMPILE_SDK_VERSION + buildToolsVersion BUILD_TOOLS_VERSION + + defaultConfig { + applicationId "rxbonjour.example" + minSdkVersion MIN_SDK_VERSION + targetSdkVersion TARGET_SDK_VERSION + versionCode 1 + versionName VERSION_NAME + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile project(':lib') + compile 'com.jakewharton:butterknife:6.1.0' + compile 'com.android.support:appcompat-v7:22.1.1' + compile 'com.android.support:recyclerview-v7:22.1.1' +} diff --git a/example/proguard-rules.pro b/example/proguard-rules.pro new file mode 100644 index 0000000..c3b64e6 --- /dev/null +++ b/example/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/marcel/Documents/android-sdk-macosx/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bbbbd71 --- /dev/null +++ b/example/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/example/src/main/java/rxbonjour/example/BonjourVH.java b/example/src/main/java/rxbonjour/example/BonjourVH.java new file mode 100644 index 0000000..1230b79 --- /dev/null +++ b/example/src/main/java/rxbonjour/example/BonjourVH.java @@ -0,0 +1,37 @@ +package rxbonjour.example; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import rxbonjour.example.rv.RvBaseHolder; +import rxbonjour.model.BonjourService; + +/** + * @author marcel + */ +public class BonjourVH extends RvBaseHolder { + + @InjectView(R.id.tv_name) TextView tvName; + @InjectView(R.id.tv_type) TextView tvType; + @InjectView(R.id.tv_host_port) TextView tvHostPort; + + /** + * Constructor + * + * @param inflater Layout inflater used to inflate the ViewHolder's layout + * @param parent Parent of the View to inflate + */ + protected BonjourVH(LayoutInflater inflater, ViewGroup parent) { + super(inflater, parent, R.layout.item_bonjourservice); + ButterKnife.inject(this, itemView); + } + + @Override protected void onBindItem(BonjourService item) { + tvName.setText(item.getName()); + tvType.setText(item.getType()); + tvHostPort.setText(item.getHost() + ":" + item.getPort()); + } +} diff --git a/example/src/main/java/rxbonjour/example/MainActivity.java b/example/src/main/java/rxbonjour/example/MainActivity.java new file mode 100644 index 0000000..d0a703a --- /dev/null +++ b/example/src/main/java/rxbonjour/example/MainActivity.java @@ -0,0 +1,116 @@ +package rxbonjour.example; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.Toast; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; +import rx.Subscription; +import rx.functions.Action1; +import rxbonjour.RxBonjour; +import rxbonjour.example.rv.RvBaseAdapter; +import rxbonjour.example.rv.RvBaseHolder; +import rxbonjour.model.BonjourEvent; +import rxbonjour.model.BonjourService; + +/** + * @author marcel + */ +public class MainActivity extends AppCompatActivity { + + @InjectView(R.id.rv) rxbonjour.example.rv.Rv rvItems; + @InjectView(R.id.et_type) EditText etInput; + private RvBaseAdapter adapter; + + private Subscription nsdSubscription; + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ButterKnife.inject(this); + + // Setup RecyclerView + rvItems.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); + adapter = new RvBaseAdapter() { + @Override protected RvBaseHolder createViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType) { + return new BonjourVH(inflater, parent); + } + }; + rvItems.setEmptyView(ButterKnife.findById(this, R.id.tv_empty)); + rvItems.setAdapter(adapter); + } + + @Override protected void onResume() { + super.onResume(); + + // Start a Bonjour lookup + restartDiscovery(); + } + + @Override protected void onPause() { + super.onPause(); + + // Unsubscribe from the network service discovery Observable + unsubscribe(); + } + + @OnClick(R.id.button_apply) void onApplyClicked() { + CharSequence input = etInput.getText(); + if (input != null && input.length() > 0) { + // For non-empty input, restart the discovery with the new input + restartDiscovery(); + } + } + + /* Begin private */ + + private void unsubscribe() { + if (nsdSubscription != null) { + nsdSubscription.unsubscribe(); + nsdSubscription = null; + } + } + + private void restartDiscovery() { + // Check the current input, only proceed if valid + String input = etInput.getText().toString(); + if (!RxBonjour.isBonjourType(input)) { + Toast.makeText(this, getString(R.string.toast_invalidtype, input), Toast.LENGTH_SHORT).show(); + return; + } + + // Cancel any previous subscription + unsubscribe(); + + // Clear the adapter's items, then start a new discovery + adapter.clearItems(); + nsdSubscription = RxBonjour.startDiscovery(this, input) + .subscribe(new Action1() { + @Override public void call(BonjourEvent bonjourEvent) { + // Depending on the type of event and the availability of the item, adjust the adapter + BonjourService item = bonjourEvent.getService(); + switch (bonjourEvent.getType()) { + case ADDED: + if (!adapter.containsItem(item)) adapter.addItem(item); + break; + + case REMOVED: + if (adapter.containsItem(item)) adapter.removeItem(item); + break; + } + } + }, new Action1() { + @Override public void call(Throwable throwable) { + Toast.makeText(MainActivity.this, throwable.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + +} diff --git a/example/src/main/java/rxbonjour/example/rv/Rv.java b/example/src/main/java/rxbonjour/example/rv/Rv.java new file mode 100644 index 0000000..53184ea --- /dev/null +++ b/example/src/main/java/rxbonjour/example/rv/Rv.java @@ -0,0 +1,151 @@ +package rxbonjour.example.rv; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.ContextMenu; +import android.view.View; + +/** + * Custom RecyclerView extension with additional properties + * + * @author marcel + */ +public class Rv extends RecyclerView { + + /** Reference to the empty view, if any */ + private View mEmptyView; + + /** Context menu info connected to this View */ + private ContextMenu.ContextMenuInfo mContextMenuInfo; + + /** Data set observer for empty view coordination */ + private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() { + @Override public void onChanged() { + checkIfEmpty(); + } + + @Override public void onItemRangeChanged(int positionStart, int itemCount) { + checkIfEmpty(); + } + + @Override public void onItemRangeInserted(int positionStart, int itemCount) { + checkIfEmpty(); + } + + @Override public void onItemRangeRemoved(int positionStart, int itemCount) { + checkIfEmpty(); + } + }; + + public Rv(Context context) { + super(context); + } + + public Rv(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public Rv(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /* Begin overrides */ + + @Override public void scrollTo(int x, int y) { + // Prevent "UnsupportedOperationException", because android:animateLayoutChanges depends on it. Works fine this way! + } + + @Override public void setAdapter(Adapter adapter) { + // First, unregister any previous adapter + Adapter oldAdapter = getAdapter(); + if (oldAdapter != null) { + oldAdapter.unregisterAdapterDataObserver(mObserver); + } + + // Call through to set the adapter + super.setAdapter(adapter); + + // Register the new adapter + if (adapter != null) { + adapter.registerAdapterDataObserver(mObserver); + } + + // Check if empty right away + checkIfEmpty(); + } + + @Override public boolean showContextMenuForChild(View originalView) { + // Initialize the context menu info for this item + try { + if (getChildAdapterPosition(originalView) != NO_POSITION) { + // Obtain the ID of the child as well and create a context menu info for it + ViewHolder holder = this.getChildViewHolder(originalView); + mContextMenuInfo = new RvContextMenuInfo(holder); + return super.showContextMenuForChild(originalView); + + } else { + return false; + } + + } catch (ClassCastException ex) { + // If the RecyclerView isn't set up for context menus + return false; + } + } + + @Override protected ContextMenu.ContextMenuInfo getContextMenuInfo() { + return mContextMenuInfo; + } + + /* Begin public */ + + /** + * Sets a reference to an empty View, which is displayed when the adapter doesn't have any items to display. + * + * @param emptyView Empty View to link to this RecyclerView + */ + public final void setEmptyView(View emptyView) { + mEmptyView = emptyView; + + // Check if empty right away + checkIfEmpty(); + } + + /* Begin private */ + + private void checkIfEmpty() { + // Only proceed if an empty view exists + Adapter adapter = getAdapter(); + if (mEmptyView != null && adapter != null) { + // Display the empty View whenever this RecyclerView doesn't show any items + boolean empty = (adapter.getItemCount() == 0); + mEmptyView.setVisibility(empty ? VISIBLE : GONE); + setVisibility(empty ? GONE : VISIBLE); + } + } + + /* Begin inner classes */ + + /** + * ContextMenuInfo implementation for a RecyclerView. Instances of this class are contained within + * the "onCreateContextMenu()" and "onContextItemSelected()" callbacks. + */ + public static class RvContextMenuInfo implements ContextMenu.ContextMenuInfo { + + public final ViewHolder holder; + public final int position; + public final long id; + + /** + * Constructor + * + * @param holder Holder from which to gather the information + */ + public RvContextMenuInfo(ViewHolder holder) { + this.holder = holder; + this.position = holder.getAdapterPosition(); + this.id = holder.getItemId(); + } + } +} diff --git a/example/src/main/java/rxbonjour/example/rv/RvBaseAdapter.java b/example/src/main/java/rxbonjour/example/rv/RvBaseAdapter.java new file mode 100644 index 0000000..0f10ab0 --- /dev/null +++ b/example/src/main/java/rxbonjour/example/rv/RvBaseAdapter.java @@ -0,0 +1,293 @@ +package rxbonjour.example.rv; + +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * RecyclerView adapter implementation with convenience methods for the insertion, deletion + * and other modifications of adapter items. + * + * @param Item type to be held by the adapter + */ +public abstract class RvBaseAdapter extends RecyclerView.Adapter> { + + /** Key which can be used for adapter items used in onSave/onRestoreInstanceState */ + protected static final String INSTANCE_STATE_ITEMS = "isitems"; + + /** Int constant to represent "no position", used when invoking indexOf() with an item not present in the adapter */ + protected static final int NO_POSITION = -1; + + /** List of unfiltered items */ + protected List mItems; + + /** Optional item click listener notified of events */ + private OnItemClickListener mOnItemClickListener; + + /** + * Constructor without any initial items to load the adapter with + */ + public RvBaseAdapter() { + // Initialize items + mItems = new ArrayList<>(); + } + + /** + * Constructor with initial items + * + * @param initialItems Initial items to load the adapter with + */ + public RvBaseAdapter(List initialItems) { + this(); + if (initialItems != null) this.mItems.addAll(initialItems); + } + + /* Begin public */ + + public void setOnItemClickListener(OnItemClickListener listener) { + this.mOnItemClickListener = listener; + } + + /** + * Returns the list of items held by this adapter + * + * @return The list of items held by the adapter + */ + public List getItems() { + return mItems; + } + + /** + * Clears the list of items held by this adapter + */ + public void clearItems() { + mItems.clear(); + notifyDataSetChanged(); + } + + /** + * Gets the item at the given position from the item list, or returns null if there is no such position. + * + * @param position Position at which to get the item + * @return The item at that position, or null for out-of-bounds values + */ + public @Nullable E getItemAt(int position) { + if (position >= 0 && position < getItemCount()) { + return mItems.get(position); + + } else { + return null; + } + } + + /** + * Obtains the index of the provided item within this adapter. + * + * @param item Item to obtain the index of within the adapter + * @return The position within the adapter's items that holds the provided item (comparisons made using equals()), or NO_POSITION if the item doesn't exist in the adapter. + */ + public int indexOf(E item) { + for (int i = 0; i < mItems.size(); i++) { + if (mItems.get(i).equals(item)) return i; + } + return NO_POSITION; + } + + /** + * Checks if the adapter contains the provided item. Ensure that custom objects implement + * equals() and hashCode() in order for this to work reliably! + * + * @param item Item to check for in the list + * @return True if the item is contained in the item list, false otherwise + */ + public boolean containsItem(E item) { + return mItems.contains(item); + } + + /** + * Re-sets the item list to the given values + * + * @param items Values to replace the adapter's current contents with + */ + public void setItems(E[] items) { + // Convert to a List and replace + this.setItems(Arrays.asList(items)); + } + + /** + * Re-sets the item list to the given values + * + * @param items Values to replace the adapter's current contents with + */ + public void setItems(Set items) { + // Convert to a List and replace + List list = new ArrayList<>(items.size()); + list.addAll(items); + this.setItems(list); + } + + /** + * Re-sets the item list to the given values + * + * @param items Values to replace the adapter's current contents with + */ + public void setItems(List items) { + // Replace the item list and notify + int oldSize = mItems.size(); + mItems = items; + notifyItemRangeChanged(0, oldSize); + } + + /** + * Appends the provided item to the end of the item list + * + * @param item Item to append to the list + */ + public void addItem(E item) { + // Simply append to the end of the list + this.insertItem(getItemCount(), item); + } + + /** + * Appends the provided items to the end of the item list + * + * @param items Items to append to the list + */ + public void addItems(Collection items) { + // Append to the end of the list + this.insertItems(getItemCount(), items); + } + + /** + * Updates the item at the given position within the item list. + * + * @param position Position at which to insert the item + * @param item Item to insert into the list + * @throws IndexOutOfBoundsException for invalid indices + */ + public void updateItem(int position, E item) { + mItems.set(position, item); + this.notifyItemChanged(position); + } + + /** + * Replaces the given item with the new one. This method appends the new item to the end of the list if the old one isn't contained in the adapter. + * @param oldItem Item to replace + * @param newItem Item to replace the old one with + */ + public void replaceItem(E oldItem, E newItem) { + // If the old item exists in the adapter, replace it. Otherwise, append the new item at the end + int index = this.indexOf(oldItem); + if (index != NO_POSITION) this.updateItem(index, newItem); + else this.addItem(newItem); + } + + /** + * Inserts the provided item at the given position within the item list. This method + * takes care of bounds-checking, so that indices outside the item list's bounds are + * automatically corrected (i.e., trying to insert at position 5 with only 1 item in the list + * resulting in the item being appended to the end of the list). + * + * @param position Position at which to insert the item + * @param item Item to insert into the list + */ + public void insertItem(int position, E item) { + // Cap the position index at 0 and the total item size, then add it + int actualPosition = Math.min(Math.max(position, 0), getItemCount()); + mItems.add(actualPosition, item); + this.notifyItemInserted(actualPosition); + } + + /** + * Inserts the provided item collection at the given position within the item list. + * This method takes care of bounds-checking, so that indices outside the item list's bounds + * are automatically corrected. + * + * @param position Position at which to insert the items + * @param items Items to insert into the list + */ + public void insertItems(int position, Collection items) { + // Cap the position index at 0 and the total item size, then add them + int actualPosition = Math.min(Math.max(position, 0), getItemCount()); + mItems.addAll(actualPosition, items); + this.notifyItemRangeInserted(actualPosition, items.size()); + } + + /** + * Removes the provided item from the item list. + * If the item doesn't exist in the list, this method does nothing. + * + * @param item Item to remove from the list + * @return True if the item could be successfully removed, false if it doesn't exist in the list + */ + public boolean removeItem(E item) { + // Find the position of the item in the list and delegate + int position = mItems.indexOf(item); + return position > -1 && this.removeItem(position); + } + + /** + * Removes the item at the given position from the item list. + * If the position is out of the item list's bounds, this method does nothing. + * + * @param position Position of the item to remove + * @return True if the item could be successfully removed, false if an out-of-bounds value was passed in + */ + public boolean removeItem(int position) { + if (position >= 0 && position < getItemCount()) { + mItems.remove(position); + this.notifyItemRemoved(position); + return true; + } + return false; + } + + /* Begin overrides */ + + @Override public int getItemCount() { + return mItems.size(); + } + + @Override public final RvBaseHolder onCreateViewHolder(ViewGroup parent, int viewType) { + // Obtain the layout inflater and delegate to the abstract creation method + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + RvBaseHolder holder = this.createViewHolder(inflater, parent, viewType); + + // Attach item click listener, if any, then return + if (mOnItemClickListener != null) holder.setItemClickListener(mOnItemClickListener); + return holder; + } + + @Override public final void onBindViewHolder(RvBaseHolder holder, int position) { + // Obtain the item to bind to the provided holder and delegate the bind process to it + E item = mItems.get(position); + holder.performBind(item); + } + + /* Begin abstract */ + + /** + * Callback method invoked upon each demand of the adapter for a new ViewHolder. + * Usually, the parameters can be passed through straight to the implementing + * {@link RvBaseHolder} class for View inflation. + * + * @param inflater Layout inflater which can be used to inflate the ViewHolder's layout + * @param parent Parent of the View to inflate + * @param viewType Type of view to inflate + * @return A new instance of the ViewHolder implementation to be used by the adapter + */ + protected abstract RvBaseHolder createViewHolder(LayoutInflater inflater, ViewGroup parent, int viewType); + + /* Begin inner classes */ + + public interface OnItemClickListener { + void onItemClick(RvBaseHolder holder, E item); + } +} \ No newline at end of file diff --git a/example/src/main/java/rxbonjour/example/rv/RvBaseHolder.java b/example/src/main/java/rxbonjour/example/rv/RvBaseHolder.java new file mode 100644 index 0000000..4b560d1 --- /dev/null +++ b/example/src/main/java/rxbonjour/example/rv/RvBaseHolder.java @@ -0,0 +1,132 @@ +package rxbonjour.example.rv; + +import android.os.Build; +import android.support.annotation.LayoutRes; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; + +/** + * RecyclerView view holder base implementation with convenience methods for click events. + * + * @param Item type to be held by the view holder + */ +public abstract class RvBaseHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + + /** The item held by the view holder */ + private E mItem; + + /** Optional item click listener */ + private RvBaseAdapter.OnItemClickListener mItemClickListener; + + /** + * Constructor + * + * @param inflater Layout inflater used to inflate the ViewHolder's layout + * @param parent Parent of the View to inflate + * @param layoutRes Resource ID of the layout with which to inflate the view holder's item view + */ + protected RvBaseHolder(LayoutInflater inflater, ViewGroup parent, @LayoutRes int layoutRes) { + // Inject the item view and call through with that + super(inflater.inflate(layoutRes, parent, false)); + + // Attach an OnClickListener to the item view + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + if (Build.VERSION.SDK_INT >= ICE_CREAM_SANDWICH) { + itemView.setOnHoverListener(new View.OnHoverListener() { + @Override public boolean onHover(View v, MotionEvent event) { + return mItem != null && RvBaseHolder.this.onHover(v, event, mItem); + } + }); + } + } + + /* Begin abstract */ + + /** + * Invoked upon binding the view holder to an item. This callback is usually used to setup the holder's UI + * with information obtained from the provided item. + * + * @param item Item with which to setup the view holder's UI components + */ + protected abstract void onBindItem(E item); + + /* Begin public */ + + /** + * Returns a reference to the currently bound item to the View Holder. + * + * @return The currently bound item + */ + public E getItem() { + return mItem; + } + + /* Begin package */ + + /** + * Internal bind method called from a {@link RvBaseAdapter}. + * + * @param item Item to bind to the view + */ + final void performBind(E item) { + // Save the item reference and call the abstract bind implementation + this.mItem = item; + this.onBindItem(item); + } + + final void setItemClickListener(RvBaseAdapter.OnItemClickListener listener) { + this.mItemClickListener = listener; + } + + /* Begin protected */ + + /** + * Invoked when the holder's item view is clicked. Does nothing by default + * + * @param v Item view of the view holder + * @param item The item it is currently bound to + */ + protected void onClick(View v, E item) { + // If an item click listener is set, call it + if (mItemClickListener != null) mItemClickListener.onItemClick(this, item); + } + + /** + * Invoked when the holder's item view is long-clicked. Returns false by default + * + * @param v Item view of the view holder + * @param item The item it is currently bound to + * @return True if the callback consumed the event, false otherwise + */ + protected boolean onLongClick(View v, E item) { + return false; + } + + /** + * Invoked when the holder's item view triggered a hover event. Returns false by default + * + * @param v Item view of the view holder + * @param event Motion event containing the hover + * @param item The item it is currently bound to + * @return True if the callback consumed the event, false otherwise + */ + protected boolean onHover(View v, MotionEvent event, E item) { + return false; + } + + /* Begin overrides */ + + @Override public final void onClick(View v) { + if (mItem != null) this.onClick(v, mItem); + } + + @Override public final boolean onLongClick(View v) { + return mItem != null && this.onLongClick(v, mItem); + } +} diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..14061bd --- /dev/null +++ b/example/src/main/res/layout/activity_main.xml @@ -0,0 +1,47 @@ + + + + + + + +