blob: ee753b8dc29189464e310842e60ee59486538e4f [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.setupwizardlib.items;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.setupwizardlib.R;
/**
* An adapter used with RecyclerView to display an {@link ItemHierarchy}. The item hierarchy used to
* create this adapter can be inflated by {@link com.android.setupwizardlib.items.ItemInflater} from
* XML.
*/
public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder>
implements ItemHierarchy.Observer {
private static final String TAG = "RecyclerItemAdapter";
/**
* A view tag set by {@link View#setTag(Object)}. If set on the root view of a layout, it will not
* create the default background for the list item. This means the item will not have ripple touch
* feedback by default.
*/
public static final String TAG_NO_BACKGROUND = "noBackground";
/** Listener for item selection in this adapter. */
public interface OnItemSelectedListener {
/**
* Called when an item in this adapter is clicked.
*
* @param item The Item corresponding to the position being clicked.
*/
void onItemSelected(IItem item);
}
private final ItemHierarchy itemHierarchy;
private OnItemSelectedListener listener;
public RecyclerItemAdapter(ItemHierarchy hierarchy) {
itemHierarchy = hierarchy;
itemHierarchy.registerObserver(this);
}
/**
* Gets the item at the given position.
*
* @see ItemHierarchy#getItemAt(int)
*/
public IItem getItem(int position) {
return itemHierarchy.getItemAt(position);
}
@Override
public long getItemId(int position) {
IItem mItem = getItem(position);
if (mItem instanceof AbstractItem) {
final int id = ((AbstractItem) mItem).getId();
return id > 0 ? id : RecyclerView.NO_ID;
} else {
return RecyclerView.NO_ID;
}
}
@Override
public int getItemCount() {
return itemHierarchy.getCount();
}
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View view = inflater.inflate(viewType, parent, false);
final ItemViewHolder viewHolder = new ItemViewHolder(view);
final Object viewTag = view.getTag();
if (!TAG_NO_BACKGROUND.equals(viewTag)) {
final TypedArray typedArray =
parent.getContext().obtainStyledAttributes(R.styleable.SuwRecyclerItemAdapter);
Drawable selectableItemBackground =
typedArray.getDrawable(
R.styleable.SuwRecyclerItemAdapter_android_selectableItemBackground);
if (selectableItemBackground == null) {
selectableItemBackground =
typedArray.getDrawable(R.styleable.SuwRecyclerItemAdapter_selectableItemBackground);
}
Drawable background = view.getBackground();
if (background == null) {
background =
typedArray.getDrawable(R.styleable.SuwRecyclerItemAdapter_android_colorBackground);
}
if (selectableItemBackground == null || background == null) {
Log.e(
TAG,
"Cannot resolve required attributes."
+ " selectableItemBackground="
+ selectableItemBackground
+ " background="
+ background);
} else {
final Drawable[] layers = {background, selectableItemBackground};
view.setBackgroundDrawable(new PatchedLayerDrawable(layers));
}
typedArray.recycle();
}
view.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
final IItem item = viewHolder.getItem();
if (listener != null && item != null && item.isEnabled()) {
listener.onItemSelected(item);
}
}
});
return viewHolder;
}
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
final IItem item = getItem(position);
holder.setEnabled(item.isEnabled());
holder.setItem(item);
item.onBindView(holder.itemView);
}
@Override
public int getItemViewType(int position) {
// Use layout resource as item view type. RecyclerView item type does not have to be
// contiguous.
IItem item = getItem(position);
return item.getLayoutResource();
}
@Override
public void onChanged(ItemHierarchy hierarchy) {
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(
ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount) {
// There is no notifyItemRangeMoved
// https://code.google.com/p/android/issues/detail?id=125984
if (itemCount == 1) {
notifyItemMoved(fromPosition, toPosition);
} else {
// If more than one, degenerate into the catch-all data set changed callback, since I'm
// not sure how recycler view handles multiple calls to notifyItemMoved (if the result
// is committed after every notification then naively calling
// notifyItemMoved(from + i, to + i) is wrong).
// Logging this in case this is a more common occurrence than expected.
Log.i(TAG, "onItemRangeMoved with more than one item");
notifyDataSetChanged();
}
}
@Override
public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
notifyItemRangeRemoved(positionStart, itemCount);
}
/**
* Find an item hierarchy within the root hierarchy.
*
* @see ItemHierarchy#findItemById(int)
*/
public ItemHierarchy findItemById(int id) {
return itemHierarchy.findItemById(id);
}
/** Gets the root item hierarchy in this adapter. */
public ItemHierarchy getRootItemHierarchy() {
return itemHierarchy;
}
/**
* Sets the listener to listen for when user clicks on a item.
*
* @see OnItemSelectedListener
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
this.listener = listener;
}
/**
* Before Lollipop, LayerDrawable always return true in getPadding, even if the children layers do
* not have any padding. Patch the implementation so that getPadding returns false if the padding
* is empty.
*
* <p>When getPadding is true, the padding of the view will be replaced by the padding of the
* drawable when {@link View#setBackgroundDrawable(Drawable)} is called. This patched class makes
* sure layer drawables without padding does not clear out original padding on the view.
*/
@VisibleForTesting
static class PatchedLayerDrawable extends LayerDrawable {
/** {@inheritDoc} */
PatchedLayerDrawable(Drawable[] layers) {
super(layers);
}
@Override
public boolean getPadding(Rect padding) {
final boolean superHasPadding = super.getPadding(padding);
return superHasPadding
&& !(padding.left == 0 && padding.top == 0 && padding.right == 0 && padding.bottom == 0);
}
}
}