package yuku.alkitab.base.widget;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ListView;
import yuku.afw.storage.Preferences;
import yuku.alkitab.base.U;
import yuku.alkitab.debug.R;
import yuku.alkitab.model.PericopeBlock;
import yuku.alkitab.model.SingleChapterVerses;
import yuku.alkitab.model.Version;
import yuku.alkitab.util.IntArrayList;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
public class VersesView extends ListView implements AbsListView.OnScrollListener {
public static final String TAG = VersesView.class.getSimpleName();
// http://stackoverflow.com/questions/6369491/stop-listview-scroll-animation
static class StopListFling {
private static Field mFlingEndField = null;
private static Method mFlingEndMethod = null;
static {
try {
mFlingEndField = AbsListView.class.getDeclaredField("mFlingRunnable");
mFlingEndField.setAccessible(true);
mFlingEndMethod = mFlingEndField.getType().getDeclaredMethod("endFling");
mFlingEndMethod.setAccessible(true);
} catch (Exception e) {
mFlingEndMethod = null;
}
}
public static void stop(ListView list) {
if (mFlingEndMethod != null) {
try {
mFlingEndMethod.invoke(mFlingEndField.get(list));
} catch (Exception ignored) {
}
}
}
}
public enum VerseSelectionMode {
none,
multiple,
singleClick,
}
public interface SelectedVersesListener {
void onSomeVersesSelected(VersesView v);
void onNoVersesSelected(VersesView v);
void onVerseSingleClick(VersesView v, int verse_1);
}
public static abstract class DefaultSelectedVersesListener implements SelectedVersesListener {
@Override public void onSomeVersesSelected(final VersesView v) {}
@Override public void onNoVersesSelected(final VersesView v) {}
@Override public void onVerseSingleClick(final VersesView v, final int verse_1) {}
}
public interface AttributeListener {
void onBookmarkAttributeClick(Version version, String versionId, int ari);
void onNoteAttributeClick(Version version, String versionId, int ari);
void onProgressMarkAttributeClick(Version version, String versionId, int preset_id);
void onHasMapsAttributeClick(Version version, String versionId, int ari);
}
public interface OnVerseScrollListener {
void onVerseScroll(VersesView v, boolean isPericope, int verse_1, float prop);
void onScrollToTop(VersesView v);
}
public enum PressKind {
left,
right,
consumed,
nop,
}
public static class PressResult {
public final PressKind kind;
public final int targetVerse_1;
public static PressResult LEFT = new PressResult(PressKind.left);
public static PressResult RIGHT = new PressResult(PressKind.right);
public static PressResult NOP = new PressResult(PressKind.nop);
private PressResult(final PressKind kind) {
this.kind = kind;
this.targetVerse_1 = 0;
}
public PressResult(final PressKind kind, final int targetVerse_1) {
this.kind = kind;
this.targetVerse_1 = targetVerse_1;
}
}
private SingleViewVerseAdapter adapter;
private SelectedVersesListener listener;
private VerseSelectionMode verseSelectionMode;
private Drawable originalSelector;
private OnVerseScrollListener onVerseScrollListener;
private AbsListView.OnScrollListener userOnScrollListener;
private int scrollState = 0;
/**
* Used as a cache, storing views to be fed to convertView parameter
* when measuring items manually at {@link #getMeasuredItemHeight(int)}.
*/
private View[] scrollToVerseConvertViews;
private String name;
private boolean firstTimeScroll = true;
/**
* Updated every time {@link #setData(int, SingleChapterVerses, int[], PericopeBlock[], int, Version, String)}
* or {@link #setDataEmpty()} is called. Used to track data changes, so delayed scroll, etc can be prevented from happening if the data has changed.
*/
private AtomicInteger dataVersionNumber = new AtomicInteger();
public VersesView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* Set the name of this VersesView for debugging.
*/
public void setName(final String name) {
this.name = name;
}
private void init() {
if (isInEditMode()) return;
originalSelector = getSelector();
setDivider(null);
setFocusable(false);
setAdapter(adapter = new SingleViewVerseAdapter(getContext()));
setOnItemClickListener(itemClick);
setVerseSelectionMode(VerseSelectionMode.multiple);
super.setOnScrollListener(this);
}
@Override public final void setOnScrollListener(AbsListView.OnScrollListener l) {
userOnScrollListener = l;
}
@Override public VerseAdapter getAdapter() {
return adapter;
}
public void setParallelListener(CallbackSpan.OnClickListener<Object> parallelListener) {
adapter.setParallelListener(parallelListener);
}
public AttributeListener getAttributeListener() {
return adapter.getAttributeListener();
}
public void setAttributeListener(final AttributeListener attributeListener) {
adapter.setAttributeListener(attributeListener);
}
public void setInlineLinkSpanFactory(final VerseInlineLinkSpan.Factory inlineLinkSpanFactory) {
adapter.setInlineLinkSpanFactory(inlineLinkSpanFactory);
}
public void setDictionaryListener(CallbackSpan.OnClickListener<SingleViewVerseAdapter.DictionaryLinkInfo> listener) {
adapter.setDictionaryListener(listener);
}
public void setVerseSelectionMode(VerseSelectionMode mode) {
this.verseSelectionMode = mode;
if (mode == VerseSelectionMode.singleClick) {
setSelector(originalSelector);
uncheckAllVerses(false);
setChoiceMode(ListView.CHOICE_MODE_NONE);
} else if (mode == VerseSelectionMode.multiple) {
setSelector(new ColorDrawable(0x0));
setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
} else if (mode == VerseSelectionMode.none) {
setSelector(new ColorDrawable(0x0));
setChoiceMode(ListView.CHOICE_MODE_NONE);
}
}
public void setOnVerseScrollListener(OnVerseScrollListener onVerseScrollListener) {
this.onVerseScrollListener = onVerseScrollListener;
}
public void reloadAttributeMap() {
adapter.reloadAttributeMap();
}
@Nullable public String getVerseText(int verse_1) {
return adapter.getVerseText(verse_1);
}
/**
* @return 1-based verse
*/
public int getVerseBasedOnScroll() {
return adapter.getVerseFromPosition(getPositionBasedOnScroll());
}
public int getPositionBasedOnScroll() {
int pos = getFirstVisiblePosition();
// check if the top one has been scrolled
View child = getChildAt(0);
if (child != null) {
int top = child.getTop();
if (top == 0) {
return pos;
}
int bottom = child.getBottom();
if (bottom > 0) {
return pos;
} else {
return pos + 1;
}
}
return pos;
}
/**
* @param version can be null if no text size multiplier is to be used
* @param versionId can be null if no text size multiplier is to be used
*/
public void setData(int ariBc, SingleChapterVerses verses, int[] pericopeAris, PericopeBlock[] pericopeBlocks, int nblock, @Nullable Version version, @Nullable String versionId) {
dataVersionNumber.incrementAndGet();
adapter.setData(ariBc, verses, pericopeAris, pericopeBlocks, nblock, version, versionId);
stopFling();
}
@Override
public void invalidateViews() {
adapter.calculateTextSizeMult();
super.invalidateViews();
}
private OnItemClickListener itemClick = new OnItemClickListener() {
@Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (verseSelectionMode == VerseSelectionMode.singleClick) {
if (listener != null) listener.onVerseSingleClick(VersesView.this, adapter.getVerseFromPosition(position));
} else if (verseSelectionMode == VerseSelectionMode.multiple) {
adapter.notifyDataSetChanged();
hideOrShowContextMenuButton();
}
}
};
public void uncheckAllVerses(boolean callListener) {
SparseBooleanArray checkedPositions = getCheckedItemPositions();
if (checkedPositions != null && checkedPositions.size() > 0) {
for (int i = checkedPositions.size() - 1; i >= 0; i--) {
if (checkedPositions.valueAt(i)) {
setItemChecked(checkedPositions.keyAt(i), false);
}
}
}
if (callListener) {
if (listener != null) listener.onNoVersesSelected(this);
}
}
public void checkVerses(IntArrayList verses_1, boolean callListener) {
uncheckAllVerses(false);
int checked_count = 0;
for (int i = 0, len = verses_1.size(); i < len; i++) {
int verse_1 = verses_1.get(i);
int count = adapter.getCount();
int pos = adapter.getPositionIgnoringPericopeFromVerse(verse_1);
if (pos != -1 && pos < count) {
setItemChecked(pos, true);
checked_count++;
}
}
if (callListener) {
if (checked_count > 0) {
if (listener != null) listener.onSomeVersesSelected(this);
} else {
if (listener != null) listener.onNoVersesSelected(this);
}
}
}
void hideOrShowContextMenuButton() {
if (verseSelectionMode != VerseSelectionMode.multiple) return;
if (getCheckedItemCount() > 0) {
if (listener != null) listener.onSomeVersesSelected(this);
} else {
if (listener != null) listener.onNoVersesSelected(this);
}
}
public IntArrayList getSelectedVerses_1() {
// count how many are selected
SparseBooleanArray positions = getCheckedItemPositions();
if (positions == null) {
return new IntArrayList(0);
}
IntArrayList res = new IntArrayList(positions.size());
for (int i = 0, len = positions.size(); i < len; i++) {
if (positions.valueAt(i)) {
int position = positions.keyAt(i);
int verse_1 = adapter.getVerseFromPosition(position);
if (verse_1 >= 1) res.add(verse_1);
}
}
return res;
}
@Override public Parcelable onSaveInstanceState() {
Bundle b = new Bundle();
Parcelable superState = super.onSaveInstanceState();
b.putParcelable("superState", superState);
b.putInt("verseSelectionMode", verseSelectionMode.ordinal());
return b;
}
@Override public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle b = (Bundle) state;
super.onRestoreInstanceState(b.getParcelable("superState"));
setVerseSelectionMode(VerseSelectionMode.values()[b.getInt("verseSelectionMode")]);
}
hideOrShowContextMenuButton();
}
public PressResult press(int keyCode) {
String volumeButtonsForNavigation = Preferences.getString(R.string.pref_volumeButtonNavigation_key, R.string.pref_volumeButtonNavigation_default);
if (U.equals(volumeButtonsForNavigation, "pasal" /* chapter */)) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return PressResult.LEFT;
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
return PressResult.RIGHT;
}
} else if (U.equals(volumeButtonsForNavigation, "ayat" /* verse */)) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) keyCode = KeyEvent.KEYCODE_DPAD_UP;
} else if (U.equals(volumeButtonsForNavigation, "page")) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
final int oldPos = getFirstVisiblePosition();
int newPos = getLastVisiblePosition();
if (oldPos == newPos && oldPos < adapter.getCount() - 1) { // in case of very long item
newPos = oldPos + 1;
}
// negate padding offset, unless this is the first item
final int paddingNegator = newPos == 0? 0 : -this.getPaddingTop();
smoothScrollFixed(newPos, paddingNegator);
return new PressResult(PressKind.consumed, adapter.getVerseFromPosition(newPos));
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
final int oldPos = getFirstVisiblePosition();
final int targetHeight = Math.max(0, getHeight() - getPaddingTop() - getPaddingBottom());
int totalHeight = 0;
// consider how long the first child has been scrolled up
final View firstChild = getChildAt(0);
if (firstChild != null) {
totalHeight += -firstChild.getTop();
}
int curPos = oldPos;
// try until totalHeight exceeds targetHeight
while (true) {
curPos--;
if (curPos < 0) {
break;
}
totalHeight += getMeasuredItemHeight(curPos);
if (totalHeight > targetHeight) {
break;
}
}
int newPos = curPos + 1;
if (oldPos == newPos && oldPos > 0) { // move at least one
newPos = oldPos - 1;
}
// negate padding offset, unless this is the first item
final int paddingNegator = newPos == 0? 0 : -this.getPaddingTop();
smoothScrollFixed(newPos, paddingNegator);
return new PressResult(PressKind.consumed, adapter.getVerseFromPosition(newPos));
}
}
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
final int oldVerse_1 = getVerseBasedOnScroll();
final int newVerse_1;
stopFling();
if (oldVerse_1 < adapter.getVerseCount()) {
newVerse_1 = oldVerse_1 + 1;
} else {
newVerse_1 = oldVerse_1;
}
scrollToVerse(newVerse_1);
return new PressResult(PressKind.consumed, newVerse_1);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
final int oldVerse_1 = getVerseBasedOnScroll();
final int newVerse_1;
stopFling();
if (oldVerse_1 > 1) { // can still go prev
newVerse_1 = oldVerse_1 - 1;
} else {
newVerse_1 = oldVerse_1;
}
scrollToVerse(newVerse_1);
return new PressResult(PressKind.consumed, newVerse_1);
}
return PressResult.NOP;
}
/**
* Fixed version of smoothScrollToPositionFromTop.
*/
private void smoothScrollFixed(final int newPos, final int offset) {
final int vn = dataVersionNumber.get();
final int smoothScrollDuration = 200; // default value (Issue 78030)
smoothScrollToPositionFromTop(newPos, offset, smoothScrollDuration);
postDelayed(() -> {
// possible that data has changed
if (vn != dataVersionNumber.get()) return;
setSelectionFromTop(newPos, offset);
}, smoothScrollDuration + 17);
}
public void setDataWithRetainSelectedVerses(boolean retainSelectedVerses, int ariBc, int[] pericope_aris, PericopeBlock[] pericope_blocks, int nblock, SingleChapterVerses verses, @NonNull Version version, @NonNull String versionId) {
IntArrayList selectedVerses_1 = null;
if (retainSelectedVerses) {
selectedVerses_1 = getSelectedVerses_1();
}
//# fill adapter with new data. make sure all checked states are reset
uncheckAllVerses(true);
setData(ariBc, verses, pericope_aris, pericope_blocks, nblock, version, versionId);
reloadAttributeMap();
boolean anySelected = false;
if (selectedVerses_1 != null) {
for (int i = 0, len = selectedVerses_1.size(); i < len; i++) {
int pos = adapter.getPositionIgnoringPericopeFromVerse(selectedVerses_1.get(i));
if (pos != -1) {
setItemChecked(pos, true);
anySelected = true;
}
}
}
if (anySelected) {
if (listener != null) listener.onSomeVersesSelected(this);
}
}
public void callAttentionForVerse(final int verse_1) {
adapter.callAttentionForVerse(verse_1);
}
/**
* This is different from {@link #scrollToVerse(int, float)} in that if the requested
* verse has a pericope header, this will scroll to the top of the pericope header,
* not to the top of the verse.
*/
public void scrollToVerse(final int verse_1) {
final int position = adapter.getPositionOfPericopeBeginningFromVerse(verse_1);
if (position == -1) {
Log.w(TAG, "could not find verse_1=" + verse_1 + ", weird!");
} else {
final int delay = firstTimeScroll? 34: 0;
final int vn = dataVersionNumber.get();
postDelayed(() -> {
// this may happen async from above, so check data version first
if (vn != dataVersionNumber.get()) return;
// negate padding offset, unless this is the first verse
final int paddingNegator = position == 0? 0 : -this.getPaddingTop();
stopFling();
setSelectionFromTop(position, paddingNegator);
firstTimeScroll = false;
}, delay);
}
}
/**
* This is different from {@link #scrollToVerse(int)} in that if the requested
* verse has a pericope header, this will scroll to the verse, ignoring the pericope header.
*/
public void scrollToVerse(int verse_1, final float prop) {
final int position = adapter.getPositionIgnoringPericopeFromVerse(verse_1);
if (position == -1) {
Log.d(TAG, "could not find verse_1: " + verse_1);
return;
}
post(() -> {
// this may happen async from above, so check first if pos is still valid
if (position >= getCount()) return;
// negate padding offset, unless this is the first verse
final int paddingNegator = position == 0? 0 : -this.getPaddingTop();
final int firstPos = getFirstVisiblePosition();
final int lastPos = getLastVisiblePosition();
if (position >= firstPos && position <= lastPos) {
// we have the child on screen, no need to measure
View child = getChildAt(position - firstPos);
stopFling();
setSelectionFromTop(position, -(int) (prop * child.getHeight()) + paddingNegator);
return;
}
final int measuredHeight = getMeasuredItemHeight(position);
stopFling();
setSelectionFromTop(position, -(int) (prop * measuredHeight) + paddingNegator);
});
}
private int getMeasuredItemHeight(final int position) {
// child needed is not on screen, we need to measure
if (scrollToVerseConvertViews == null) {
// initialize scrollToVerseConvertViews if needed
scrollToVerseConvertViews = new View[adapter.getViewTypeCount()];
}
final int itemType = adapter.getItemViewType(position);
final View convertView = scrollToVerseConvertViews[itemType];
final View child = adapter.getView(position, convertView, this);
child.measure(MeasureSpec.makeMeasureSpec(this.getWidth() - this.getPaddingLeft() - this.getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
scrollToVerseConvertViews[itemType] = child;
return child.getMeasuredHeight();
}
public void scrollToTop() {
post(() -> setSelectionFromTop(0, 0));
}
public void setSelectedVersesListener(SelectedVersesListener listener) {
this.listener = listener;
}
@Override public void onScrollStateChanged(AbsListView view, int scrollState) {
if (userOnScrollListener != null) userOnScrollListener.onScrollStateChanged(view, scrollState);
this.scrollState = scrollState;
}
@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (userOnScrollListener != null) userOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
if (onVerseScrollListener == null) return;
if (view.getChildCount() == 0) return;
float prop = 0.f;
int position = -1;
final View firstChild = view.getChildAt(0);
final int remaining = firstChild.getBottom(); // padding top is ignored
if (remaining >= 0) { // bottom of first child is lower than top padding
position = firstVisibleItem;
prop = 1.f - (float) remaining / firstChild.getHeight();
} else { // we should have a second child
if (view.getChildCount() > 1) {
final View secondChild = view.getChildAt(1);
position = firstVisibleItem + 1;
prop = (float) -remaining / secondChild.getHeight();
}
}
final int verse_1 = adapter.getVerseOrPericopeFromPosition(position);
if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
if (verse_1 > 0) {
onVerseScrollListener.onVerseScroll(this, false, verse_1, prop);
} else {
onVerseScrollListener.onVerseScroll(this, true, 0, 0);
}
if (position == 0 && firstChild.getTop() == this.getPaddingTop()) {
// we are really at the top
onVerseScrollListener.onScrollToTop(this);
}
}
}
public void setDataEmpty() {
dataVersionNumber.incrementAndGet();
adapter.setDataEmpty();
}
public void stopFling() {
StopListFling.stop(this);
}
@Override
public String toString() {
return name != null? ("VersesView{name=" + name + "}"): "VersesView";
}
public void setDictionaryModeAris(@Nullable final SparseBooleanArray aris) {
adapter.setDictionaryModeAris(aris);
}
}