package com.frozendevs.periodictable.view;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.util.LruCache;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.BaseAdapter;
import com.frozendevs.periodictable.R;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
public class PeriodicTableView extends ZoomableScrollView {
private final float DEFAULT_SPACING = 1f;
private View mEmptyView = null;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private Adapter mAdapter;
private Matrix mMatrix = new Matrix();
private OnItemClickListener mOnItemClickListener;
private View mActiveView;
private LruCache<Integer, Bitmap> mBitmapCache;
private int mTileSize;
private GenerateBitmapsTask mGenerateBitmapsTask;
public interface OnItemClickListener {
void onItemClick(PeriodicTableView parent, View view, int position);
}
public static abstract class Adapter extends BaseAdapter {
public abstract View getActiveView(Bitmap bitmap, View convertView, ViewGroup parent);
public abstract int getGroupsCount();
public abstract int getPeriodsCount();
}
private abstract class OnClickConfirmedListener {
abstract void onClickConfirmed(int position);
}
private DataSetObserver mDataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
updateEmptyStatus(true);
if (mAdapter.isEmpty()) {
return;
}
if (mBitmapCache.size() < mAdapter.getCount()) {
if (mGenerateBitmapsTask != null) {
mGenerateBitmapsTask.cancel(true);
}
mGenerateBitmapsTask = new GenerateBitmapsTask();
mGenerateBitmapsTask.execute();
} else {
onGenerateComplete();
}
}
};
private class GenerateBitmapsTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
final int size = mAdapter.getGroupsCount() * mAdapter.getPeriodsCount();
final Map<Integer, SoftReference<View>> convertViews = new HashMap<>();
for (int position = 0; position < size; position++) {
if (isCancelled()) {
return null;
}
if (mBitmapCache.get(position) != null) {
continue;
}
final int viewType = mAdapter.getItemViewType(position);
View convertView = null;
final SoftReference<View> softReference = convertViews.get(viewType);
if (softReference != null) {
convertView = softReference.get();
}
convertView = mAdapter.getView(position, convertView, PeriodicTableView.this);
if (convertView != null) {
final Bitmap bitmap = generateBitmap(convertView);
if (bitmap != null) {
mBitmapCache.put(position, bitmap);
}
}
if (softReference == null || softReference.get() == null) {
convertViews.put(viewType, new SoftReference<>(convertView));
}
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (!isCancelled()) {
onGenerateComplete();
}
}
private Bitmap generateBitmap(View view) {
Bitmap bitmap = null;
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
mTileSize = Math.max(mTileSize, Math.max(layoutParams.width,
layoutParams.height));
view.measure(View.MeasureSpec.makeMeasureSpec(layoutParams.width,
View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(
layoutParams.height, View.MeasureSpec.EXACTLY));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.buildDrawingCache();
final Bitmap drawingCache = view.getDrawingCache();
if (drawingCache != null) {
bitmap = Bitmap.createBitmap(drawingCache);
}
view.destroyDrawingCache();
return bitmap;
}
}
private static class SavedState extends BaseSavedState {
int activeViewPosition = -1;
int tileSize;
public SavedState(Parcel source) {
super(source);
activeViewPosition = source.readInt();
tileSize = source.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(activeViewPosition);
out.writeInt(tileSize);
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
private OnClickConfirmedListener mOnSingleTapConfirmed = new OnClickConfirmedListener() {
@Override
void onClickConfirmed(int position) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnItemClickListener.onItemClick(PeriodicTableView.this, mActiveView, position);
if (mActiveView != null) {
mActiveView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
};
private OnClickConfirmedListener mOnDownConfirmed;
public PeriodicTableView(Context context) {
super(context);
initPeriodicTableView();
}
public PeriodicTableView(Context context, AttributeSet attrs) {
super(context, attrs);
initPeriodicTableView();
}
public PeriodicTableView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPeriodicTableView();
}
private void initPeriodicTableView() {
mOnDownConfirmed = new OnClickConfirmedListener() {
@Override
void onClickConfirmed(int position) {
addActiveView(position);
}
};
}
public void setEmptyView(View view) {
mEmptyView = view;
updateEmptyStatus(mAdapter == null || mAdapter.isEmpty() || mBitmapCache == null ||
mBitmapCache.size() < mAdapter.getCount());
}
private void updateEmptyStatus(boolean empty) {
if (mEmptyView != null) {
mEmptyView.setVisibility(empty ? VISIBLE : GONE);
}
}
private float getScaledTileSize() {
return getZoom() * mTileSize;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent event) {
return mOnItemClickListener != null && processClick(event, mOnSingleTapConfirmed);
}
@Override
protected int getScaledWidth() {
int groups = mAdapter != null ? mAdapter.getGroupsCount() : 0;
return Math.round((getScaledTileSize() * groups) + ((groups - 1) * DEFAULT_SPACING));
}
@Override
protected int getScaledHeight() {
int periods = mAdapter != null ? mAdapter.getPeriodsCount() : 0;
return Math.round((getScaledTileSize() * periods) +
((periods - 1) * DEFAULT_SPACING));
}
public void setAdapter(Adapter adapter) {
if (adapter != null && mBitmapCache == null) {
throw new IllegalStateException("Initialize bitmap cache using setBitmapCache() first");
}
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
mAdapter = adapter;
if (mAdapter != null) {
mAdapter.registerDataSetObserver(mDataSetObserver);
}
}
@Override
public float getMinimalZoom() {
final int groups = mAdapter.getGroupsCount();
final int periods = mAdapter.getPeriodsCount();
final int tileSize = mTileSize;
return Math.min((getWidth() - ((groups - 1) * DEFAULT_SPACING)) / groups,
(getHeight() - ((periods - 1) * DEFAULT_SPACING)) / periods) / tileSize;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
}
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
@Override
public void onDraw(Canvas canvas) {
adjustActiveView();
super.onDraw(canvas);
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mBitmapCache != null && mAdapter != null && !mAdapter.isEmpty()) {
float tileSize = getScaledTileSize();
float y = (getHeight() - getScaledHeight()) / 2f;
for (int row = 0; row < mAdapter.getPeriodsCount(); row++) {
float x = (getWidth() - getScaledWidth()) / 2f;
for (int column = 0; column < mAdapter.getGroupsCount(); column++) {
if (x + tileSize > getScrollX() && x < getScrollX() + getWidth() &&
y + tileSize > getScrollY() && y < getScrollY() + getHeight()) {
int position = (row * mAdapter.getGroupsCount()) + column;
if (mActiveView != null && indexOfChild(mActiveView) >= 0 &&
position == (int) mActiveView.getTag(R.id.active_view_position)) {
adjustActiveView();
} else {
Bitmap bitmap = mBitmapCache.get(position);
if (bitmap != null && !bitmap.isRecycled()) {
mMatrix.reset();
mMatrix.postScale(getZoom(), getZoom());
mMatrix.postTranslate(x, y);
canvas.drawBitmap(bitmap, mMatrix, mPaint);
}
}
}
x += tileSize + DEFAULT_SPACING;
}
y += tileSize + DEFAULT_SPACING;
}
}
super.dispatchDraw(canvas);
}
private void adjustActiveView() {
if (mActiveView != null) {
int position = (int) mActiveView.getTag(R.id.active_view_position);
float tileSize = getScaledTileSize();
mActiveView.setScaleX(getZoom());
mActiveView.setScaleY(getZoom());
mActiveView.setTranslationX(((getWidth() - getScaledWidth()) / 2f) +
((position % mAdapter.getGroupsCount()) * (tileSize + DEFAULT_SPACING)));
mActiveView.setTranslationY(((getHeight() - getScaledHeight()) / 2f) +
((position / mAdapter.getGroupsCount()) * (tileSize + DEFAULT_SPACING)));
}
}
private void addActiveView(int position) {
if (mActiveView != null) {
if (position == (int) mActiveView.getTag(R.id.active_view_position)) {
adjustActiveView();
return;
}
removeView(mActiveView);
}
mActiveView = mAdapter.getActiveView(mBitmapCache.get(position), mActiveView, this);
if (mActiveView != null) {
mActiveView.setTag(R.id.active_view_position, position);
mActiveView.setPivotX(0f);
mActiveView.setPivotY(0f);
adjustActiveView();
addView(mActiveView);
}
}
@Override
public Parcelable onSaveInstanceState() {
SavedState savedState = new SavedState(super.onSaveInstanceState());
savedState.tileSize = mTileSize;
if (mActiveView != null) {
savedState.activeViewPosition = (int) mActiveView.getTag(R.id.active_view_position);
}
return savedState;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
SavedState savedState = (SavedState) state;
mTileSize = savedState.tileSize;
if (!mAdapter.isEmpty() && savedState.activeViewPosition > -1) {
addActiveView(savedState.activeViewPosition);
}
super.onRestoreInstanceState(savedState.getSuperState());
} else {
super.onRestoreInstanceState(state);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
adjustActiveView();
}
public View getActiveView() {
return mActiveView;
}
@Override
public boolean onDown(MotionEvent event) {
super.onDown(event);
return processClick(event, mOnDownConfirmed);
}
private boolean processClick(MotionEvent event, OnClickConfirmedListener listener) {
if (listener != null && mAdapter != null && !mAdapter.isEmpty()) {
final float rawX = event.getX() + getScrollX();
final float rawY = event.getY() + getScrollY();
final float tileSize = getScaledTileSize();
final int scaledWidth = getScaledWidth();
final int scaledHeight = getScaledHeight();
final float startY = (getHeight() - scaledHeight) / 2f;
final float startX = (getWidth() - scaledWidth) / 2f;
if (rawX >= startX && rawX <= startX + scaledWidth &&
rawY >= startY && rawY <= startY + scaledHeight) {
final int position = ((int) ((rawY - startY) / (tileSize + DEFAULT_SPACING)) *
mAdapter.getGroupsCount()) + (int) ((rawX - startX) / (tileSize + DEFAULT_SPACING));
final int size = mAdapter.getGroupsCount() * mAdapter.getPeriodsCount();
if (position >= 0 && position < size && mAdapter.isEnabled(position)) {
listener.onClickConfirmed(position);
return true;
}
}
}
return false;
}
public void setBitmapCache(LruCache<Integer, Bitmap> bitmapCache) {
mBitmapCache = bitmapCache;
}
private void onGenerateComplete() {
if (mActiveView == null) {
final int size = mAdapter.getGroupsCount() * mAdapter.getPeriodsCount();
for (int i = 0; i < size; i++) {
if (mAdapter.getItem(i) != null && mAdapter.isEnabled(i)) {
addActiveView(i);
break;
}
}
}
invalidate();
updateEmptyStatus(false);
}
}