diff --git a/app/build.gradle b/app/build.gradle index d3c6af9..dbdf47a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,8 +154,6 @@ dependencies { implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.3' - implementation 'me.gujun.android.taggroup:library:1.4@aar' - implementation 'uk.co.chrisjenx:calligraphy:2.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' diff --git a/app/src/main/assets/updatelog.fy b/app/src/main/assets/updatelog.fy index d65ad54..95a21ea 100644 --- a/app/src/main/assets/updatelog.fy +++ b/app/src/main/assets/updatelog.fy @@ -1,3 +1,7 @@ +风月读书v2.1.4 +更新内容: +1、搜索新增关键词推荐补全 + 2021.06.15 风月读书v2.1.3 更新内容: diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java index 42f3a8c..1c8c7dd 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java @@ -1,14 +1,15 @@ package xyz.fycz.myreader.ui.activity; -import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.Color; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; import android.text.Editable; +import android.text.SpannableString; +import android.text.Spanned; import android.text.TextWatcher; +import android.text.style.ForegroundColorSpan; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; @@ -34,6 +35,7 @@ import java.util.Map; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; +import io.reactivex.disposables.Disposable; import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.App; import xyz.fycz.myreader.application.SysManager; @@ -56,8 +58,8 @@ import xyz.fycz.myreader.ui.adapter.SearchHistoryAdapter; import xyz.fycz.myreader.ui.dialog.DialogCreator; import xyz.fycz.myreader.ui.dialog.MultiChoiceDialog; import xyz.fycz.myreader.util.SharedPreUtils; -import xyz.fycz.myreader.util.help.StringHelper; import xyz.fycz.myreader.util.ToastUtils; +import xyz.fycz.myreader.util.help.StringHelper; import xyz.fycz.myreader.util.utils.OkHttpUtils; import xyz.fycz.myreader.util.utils.RxUtils; import xyz.fycz.myreader.webapi.crawler.ReadCrawlerUtil; @@ -73,10 +75,11 @@ public class SearchBookActivity extends BaseActivity { private SearchBookAdapter mSearchBookAdapter; private String searchKey;//搜索关键字 - private ArrayList mBooksBean = new ArrayList<>(); + private List mBooksBean = new ArrayList<>(); private ConMVMap mBooks = new ConMVMap<>(); - private ArrayList mSearchHistories = new ArrayList<>(); - private ArrayList mSuggestions = new ArrayList<>(); + private List mSearchHistories = new ArrayList<>(); + private List mSuggestions = new ArrayList<>(); + private List mHotKeys = new ArrayList<>(); private SearchHistoryService mSearchHistoryService; @@ -92,32 +95,14 @@ public class SearchBookActivity extends BaseActivity { private AlertDialog mDisableSourceDia; + private Disposable sugDis; - private static String[] suggestion = {"第一序列", "大道朝天", "伏天氏", "终极斗罗", "我师兄实在太稳健了", "烂柯棋缘", "诡秘之主"}; - private static String[] suggestion2 = {"不朽凡人", "圣墟", "我是至尊", "龙王传说", "太古神王", "一念永恒", "雪鹰领主", "大主宰"}; + private boolean showBooks; - private boolean showHot; - @SuppressLint("HandlerLeak") - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case 1: - search(); - break; - case 2: - binding.srlSearchBookList.finishRefresh(); - /*if (curThreadCount == 0 && !isStopSearch) { - rpb.setIsAutoLoading(false); - }*/ - break; - case 3: - binding.fabSearchStop.setVisibility(View.GONE); - break; - } - } - }; + private static String[] suggestion = {"第一序列", "大道朝天", "伏天氏", "终极斗罗", "我师兄实在太稳健了", "烂柯棋缘", "诡秘之主", "不朽凡人", "圣墟", "我是至尊", "龙王传说", "太古神王", "一念永恒", "雪鹰领主", "大主宰"}; + + private boolean showHot; @Override @@ -151,7 +136,7 @@ public class SearchBookActivity extends BaseActivity { public void loadMoreSearchBook(ConMVMap items) { mBooks.addAll(items); mSearchBookAdapter.addAll(new ArrayList<>(items.keySet()), searchKey); - mHandler.sendMessage(mHandler.obtainMessage(2)); + binding.srlSearchBookList.finishRefresh(); } @Override @@ -161,20 +146,20 @@ public class SearchBookActivity extends BaseActivity { @Override public void searchBookError(Throwable throwable) { - mHandler.sendMessage(mHandler.obtainMessage(2)); + binding.srlSearchBookList.finishRefresh(); } }); + initHotKeys(); } @Override protected void initWidget() { super.initWidget(); - initSuggestionList(); //enter事件 binding.etSearchKey.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_UNSPECIFIED) { - mHandler.sendMessage(mHandler.obtainMessage(1)); + search(); return (keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER); } return false; @@ -228,9 +213,9 @@ public class SearchBookActivity extends BaseActivity { public void afterTextChanged(final Editable editable) { searchKey = editable.toString(); if (StringHelper.isEmpty(searchKey)) { - mHandler.sendMessage(mHandler.obtainMessage(1)); + search(); } - + initSuggestionList(); } }); @@ -240,10 +225,10 @@ public class SearchBookActivity extends BaseActivity { //上拉刷新 binding.srlSearchBookList.setOnRefreshListener(refreshLayout -> { stopSearch(); - mHandler.sendMessage(mHandler.obtainMessage(1)); + search(); }); initHistoryList(); - mHandler.postDelayed(() -> { + App.getHandler().postDelayed(() -> { binding.etSearchKey.requestFocus(); InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); @@ -258,18 +243,18 @@ public class SearchBookActivity extends BaseActivity { binding.llRefreshSuggestBooks.setOnClickListener(new RenewSuggestionBook()); //搜索按钮点击事件 - binding.tvSearchConform.setOnClickListener(view -> mHandler.sendMessage(mHandler.obtainMessage(1))); + binding.tvSearchConform.setOnClickListener(view -> search()); //suggestion搜索事件 binding.tgSuggestBook.setOnTagClickListener(tag -> { binding.etSearchKey.setText(tag); binding.etSearchKey.setSelection(tag.length()); - mHandler.sendMessage(mHandler.obtainMessage(1)); + search(); }); //历史记录搜索事件 binding.lvHistoryList.setOnItemClickListener((parent, view, position, id) -> { binding.etSearchKey.setText(mSearchHistories.get(position).getContent()); binding.etSearchKey.setSelection(mSearchHistories.get(position).getContent().length()); - mHandler.sendMessage(mHandler.obtainMessage(1)); + search(); }); //清空历史记录 binding.llClearHistory.setOnClickListener(v -> { @@ -312,7 +297,7 @@ public class SearchBookActivity extends BaseActivity { public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_hot) { showHot = !showHot; - initSuggestionList(); + initHotKeys(); } else if (item.getItemId() == R.id.action_disable_source) { showDisableSourceDia(); } else if (item.getItemId() == R.id.action_source_man) { @@ -407,15 +392,14 @@ public class SearchBookActivity extends BaseActivity { }); } - /** - * 初始化建议书目 - */ - private void initSuggestionList() { + private void initHotKeys() { + mHotKeys.clear(); if (!showHot) { - binding.tgSuggestBook.setTags(suggestion); + mHotKeys.addAll(Arrays.asList(suggestion)); + initSuggestionList(); } else { SharedPreUtils spu = SharedPreUtils.getInstance(); - Single.create((SingleOnSubscribe) emitter -> { + Single.create((SingleOnSubscribe) emitter -> { String cookie = spu.getString(getString(R.string.qdCookie), ""); String url = "https://m.qidian.com/majax/search/auto?kw=&"; if (cookie.equals("")) { @@ -424,17 +408,55 @@ public class SearchBookActivity extends BaseActivity { url += cookie.split(";")[0]; Map headers = new HashMap<>(); headers.put("Cookie", cookie); - emitter.onSuccess(OkHttpUtils.getHtml(url, null, - "utf-8", headers)); + emitter.onSuccess(parseHotKeys(OkHttpUtils.getHtml(url, null, "utf-8", headers))); + }).compose(RxUtils::toSimpleSingle).subscribe(new MySingleObserver() { + @Override + public void onSuccess(@NotNull Boolean b) { + initSuggestionList(); + } + + @Override + public void onError(Throwable e) { + mHotKeys.addAll(Arrays.asList(suggestion)); + initSuggestionList(); + } + }); + ; + } + } + + /** + * 初始化建议书目 + */ + private void initSuggestionList() { + if (showBooks) return; + if (sugDis != null) { + sugDis.dispose(); + } + mSuggestions.clear(); + if (StringHelper.isEmpty(searchKey)) { + if (mHotKeys.isEmpty()) { + binding.llSuggestBooksView.setVisibility(View.GONE); + } else { + binding.llSuggestBooksView.setVisibility(View.VISIBLE); + binding.llRefreshSuggestBooks.setVisibility(View.VISIBLE); + binding.tgSuggestBook.setTags2(mHotKeys.subList(0, mHotKeys.size() / 2)); + } + } else { + String url = "https://newzxautocmp.reader.qq.com/BookSuggAll?key=" + searchKey; + Single.create((SingleOnSubscribe) emitter -> { + emitter.onSuccess(OkHttpUtils.getHtml(url)); }).compose(RxUtils::toSimpleSingle).subscribe(new MySingleObserver() { + @Override + public void onSubscribe(Disposable d) { + sugDis = d; + } + @Override public void onSuccess(@NotNull String s) { - parseSuggestionList(s); - if (mSuggestions.size() > 0) { - binding.tgSuggestBook.setTags(mSuggestions.subList(0, mSuggestions.size() / 2)); - } else { - binding.llSuggestBooksView.setVisibility(View.GONE); - } + parseSuggListByKey(s); + binding.llRefreshSuggestBooks.setVisibility(View.GONE); + binding.tgSuggestBook.setTags2(mSuggestions); } @Override @@ -445,12 +467,35 @@ public class SearchBookActivity extends BaseActivity { } } - private void parseSuggestionList(String jsonStr) { + private boolean parseHotKeys(String jsonStr) { try { JSONObject json = new JSONObject(jsonStr); JSONArray names = json.getJSONObject("data").getJSONArray("popWords"); for (int i = 0; i < names.length(); i++) { - mSuggestions.add(names.getJSONObject(i).getString("name")); + mHotKeys.add(names.getJSONObject(i).getString("name")); + } + return true; + } catch (JSONException e) { + e.printStackTrace(); + } + return false; + } + + private void parseSuggListByKey(String jsonStr) { + try { + JSONObject json = new JSONObject(jsonStr); + JSONArray names = json.getJSONArray("matchList"); + for (int i = 0; i < names.length(); i++) { + String title = names.getJSONObject(i).getString("title"); + int start = title.indexOf(searchKey); + if (start != -1) { + SpannableString spannableString = new SpannableString(title); + spannableString.setSpan(new ForegroundColorSpan(Color.RED), + start, start + searchKey.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + mSuggestions.add(spannableString); + } else { + mSuggestions.add(title); + } } } catch (JSONException e) { e.printStackTrace(); @@ -460,21 +505,12 @@ public class SearchBookActivity extends BaseActivity { private class RenewSuggestionBook implements View.OnClickListener { @Override public void onClick(View v) { - if (!showHot) { + if (mHotKeys.size() > 0) { String[] s = binding.tgSuggestBook.getTags(); - if (Arrays.equals(s, suggestion)) { - binding.tgSuggestBook.setTags(suggestion2); + if (s[0].equals(mHotKeys.get(0))) { + binding.tgSuggestBook.setTags2(mHotKeys.subList(mHotKeys.size() / 2, mHotKeys.size())); } else { - binding.tgSuggestBook.setTags(suggestion); - } - } else { - if (mSuggestions.size() > 0) { - String[] s = binding.tgSuggestBook.getTags(); - if (s[0].equals(mSuggestions.get(0))) { - binding.tgSuggestBook.setTags(mSuggestions.subList(mSuggestions.size() / 2, mSuggestions.size())); - } else { - binding.tgSuggestBook.setTags(mSuggestions.subList(0, mSuggestions.size() / 2)); - } + binding.tgSuggestBook.setTags2(mHotKeys.subList(0, mHotKeys.size() / 2)); } } } @@ -501,7 +537,6 @@ public class SearchBookActivity extends BaseActivity { //initmBooksBean(); binding.rvSearchBooksList.setVisibility(View.VISIBLE); binding.llSuggestBooksView.setVisibility(View.GONE); - binding.llSuggestBooksView.setVisibility(View.GONE); } @@ -533,6 +568,7 @@ public class SearchBookActivity extends BaseActivity { private void search() { binding.rpb.setIsAutoLoading(true); if (StringHelper.isEmpty(searchKey)) { + showBooks = false; stopSearch(); binding.rpb.setIsAutoLoading(false); binding.rvSearchBooksList.setVisibility(View.GONE); @@ -542,6 +578,7 @@ public class SearchBookActivity extends BaseActivity { binding.rvSearchBooksList.setAdapter(null); binding.srlSearchBookList.setEnableRefresh(false); } else { + showBooks = true; mSearchBookAdapter = new SearchBookAdapter(this, mBooks, searchEngine, searchKey); binding.rvSearchBooksList.setAdapter(mSearchBookAdapter); //进入书籍详情页 @@ -569,7 +606,7 @@ public class SearchBookActivity extends BaseActivity { private void stopSearch() { searchEngine.stopSearch(); - mHandler.sendEmptyMessage(3); + binding.fabSearchStop.setVisibility(View.GONE); } @Override @@ -584,9 +621,6 @@ public class SearchBookActivity extends BaseActivity { @Override protected void onDestroy() { stopSearch(); - for (int i = 0; i < 9; i++) { - mHandler.removeMessages(i + 1); - } super.onDestroy(); } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchHistoryAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchHistoryAdapter.java index 82f23a0..137fac0 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchHistoryAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchHistoryAdapter.java @@ -13,12 +13,13 @@ import xyz.fycz.myreader.R; import xyz.fycz.myreader.greendao.entity.SearchHistory; import java.util.ArrayList; +import java.util.List; public class SearchHistoryAdapter extends ArrayAdapter { private int mResourceId; - public SearchHistoryAdapter(Context context, int resourceId, ArrayList datas){ + public SearchHistoryAdapter(Context context, int resourceId, List datas){ super(context,resourceId,datas); mResourceId = resourceId; } diff --git a/app/src/main/java/xyz/fycz/myreader/widget/TagGroup.java b/app/src/main/java/xyz/fycz/myreader/widget/TagGroup.java new file mode 100644 index 0000000..472d708 --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/widget/TagGroup.java @@ -0,0 +1,1042 @@ +package xyz.fycz.myreader.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathEffect; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.ArrowKeyMovementMethod; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionWrapper; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import xyz.fycz.myreader.R; + +/** + * A TagGroup is a special layout with a set of tags. + * This group has two modes: + *

+ * 1. APPEND mode + * 2. DISPLAY mode + *

+ * Default is DISPLAY mode. When in APPEND mode, the group is capable of input for append new tags + * and delete tags. + *

+ * When in DISPLAY mode, the group is only contain NORMAL state tags, and the tags in group + * is not focusable. + *

+ * + * @author Jun Gu (http://2dxgujun.com) + * @version 2.0 + * @since 2015-2-3 14:16:32 + */ +public class TagGroup extends ViewGroup { + private final int default_border_color = Color.rgb(0x49, 0xC1, 0x20); + private final int default_text_color = Color.rgb(0x49, 0xC1, 0x20); + private final int default_background_color = Color.WHITE; + private final int default_dash_border_color = Color.rgb(0xAA, 0xAA, 0xAA); + private final int default_input_hint_color = Color.argb(0x80, 0x00, 0x00, 0x00); + private final int default_input_text_color = Color.argb(0xDE, 0x00, 0x00, 0x00); + private final int default_checked_border_color = Color.rgb(0x49, 0xC1, 0x20); + private final int default_checked_text_color = Color.WHITE; + private final int default_checked_marker_color = Color.WHITE; + private final int default_checked_background_color = Color.rgb(0x49, 0xC1, 0x20); + private final int default_pressed_background_color = Color.rgb(0xED, 0xED, 0xED); + private final float default_border_stroke_width; + private final float default_text_size; + private final float default_horizontal_spacing; + private final float default_vertical_spacing; + private final float default_horizontal_padding; + private final float default_vertical_padding; + + /** Indicates whether this TagGroup is set up to APPEND mode or DISPLAY mode. Default is false. */ + private boolean isAppendMode; + + /** The text to be displayed when the text of the INPUT tag is empty. */ + private CharSequence inputHint; + + /** The tag outline border color. */ + private int borderColor; + + /** The tag text color. */ + private int textColor; + + /** The tag background color. */ + private int backgroundColor; + + /** The dash outline border color. */ + private int dashBorderColor; + + /** The input tag hint text color. */ + private int inputHintColor; + + /** The input tag type text color. */ + private int inputTextColor; + + /** The checked tag outline border color. */ + private int checkedBorderColor; + + /** The check text color */ + private int checkedTextColor; + + /** The checked marker color. */ + private int checkedMarkerColor; + + /** The checked tag background color. */ + private int checkedBackgroundColor; + + /** The tag background color, when the tag is being pressed. */ + private int pressedBackgroundColor; + + /** The tag outline border stroke width, default is 0.5dp. */ + private float borderStrokeWidth; + + /** The tag text size, default is 13sp. */ + private float textSize; + + /** The horizontal tag spacing, default is 8.0dp. */ + private int horizontalSpacing; + + /** The vertical tag spacing, default is 4.0dp. */ + private int verticalSpacing; + + /** The horizontal tag padding, default is 12.0dp. */ + private int horizontalPadding; + + /** The vertical tag padding, default is 3.0dp. */ + private int verticalPadding; + + /** Listener used to dispatch tag change event. */ + private OnTagChangeListener mOnTagChangeListener; + + /** Listener used to dispatch tag click event. */ + private OnTagClickListener mOnTagClickListener; + + /** Listener used to handle tag click event. */ + private InternalTagClickListener mInternalTagClickListener = new InternalTagClickListener(); + + public TagGroup(Context context) { + this(context, null); + } + + public TagGroup(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.tagGroupStyle); + } + + public TagGroup(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + default_border_stroke_width = dp2px(0.5f); + default_text_size = sp2px(13.0f); + default_horizontal_spacing = dp2px(8.0f); + default_vertical_spacing = dp2px(4.0f); + default_horizontal_padding = dp2px(12.0f); + default_vertical_padding = dp2px(3.0f); + + // Load styled attributes. + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TagGroup, defStyleAttr, R.style.TagGroup); + try { + isAppendMode = a.getBoolean(R.styleable.TagGroup_atg_isAppendMode, false); + inputHint = a.getText(R.styleable.TagGroup_atg_inputHint); + borderColor = a.getColor(R.styleable.TagGroup_atg_borderColor, default_border_color); + textColor = a.getColor(R.styleable.TagGroup_atg_textColor, default_text_color); + backgroundColor = a.getColor(R.styleable.TagGroup_atg_backgroundColor, default_background_color); + dashBorderColor = a.getColor(R.styleable.TagGroup_atg_dashBorderColor, default_dash_border_color); + inputHintColor = a.getColor(R.styleable.TagGroup_atg_inputHintColor, default_input_hint_color); + inputTextColor = a.getColor(R.styleable.TagGroup_atg_inputTextColor, default_input_text_color); + checkedBorderColor = a.getColor(R.styleable.TagGroup_atg_checkedBorderColor, default_checked_border_color); + checkedTextColor = a.getColor(R.styleable.TagGroup_atg_checkedTextColor, default_checked_text_color); + checkedMarkerColor = a.getColor(R.styleable.TagGroup_atg_checkedMarkerColor, default_checked_marker_color); + checkedBackgroundColor = a.getColor(R.styleable.TagGroup_atg_checkedBackgroundColor, default_checked_background_color); + pressedBackgroundColor = a.getColor(R.styleable.TagGroup_atg_pressedBackgroundColor, default_pressed_background_color); + borderStrokeWidth = a.getDimension(R.styleable.TagGroup_atg_borderStrokeWidth, default_border_stroke_width); + textSize = a.getDimension(R.styleable.TagGroup_atg_textSize, default_text_size); + horizontalSpacing = (int) a.getDimension(R.styleable.TagGroup_atg_horizontalSpacing, default_horizontal_spacing); + verticalSpacing = (int) a.getDimension(R.styleable.TagGroup_atg_verticalSpacing, default_vertical_spacing); + horizontalPadding = (int) a.getDimension(R.styleable.TagGroup_atg_horizontalPadding, default_horizontal_padding); + verticalPadding = (int) a.getDimension(R.styleable.TagGroup_atg_verticalPadding, default_vertical_padding); + } finally { + a.recycle(); + } + + if (isAppendMode) { + // Append the initial INPUT tag. + appendInputTag(); + + // Set the click listener to detect the end-input event. + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + submitTag(); + } + }); + } + } + + /** + * Call this to submit the INPUT tag. + */ + public void submitTag() { + final TagView inputTag = getInputTag(); + if (inputTag != null && inputTag.isInputAvailable()) { + inputTag.endInput(); + + if (mOnTagChangeListener != null) { + mOnTagChangeListener.onAppend(TagGroup.this, inputTag.getText().toString()); + } + appendInputTag(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + measureChildren(widthMeasureSpec, heightMeasureSpec); + + int width = 0; + int height = 0; + + int row = 0; // The row counter. + int rowWidth = 0; // Calc the current row width. + int rowMaxHeight = 0; // Calc the max tag height, in current row. + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + if (child.getVisibility() != GONE) { + rowWidth += childWidth; + if (rowWidth > widthSize) { // Next line. + rowWidth = childWidth; // The next row width. + height += rowMaxHeight + verticalSpacing; + rowMaxHeight = childHeight; // The next row max height. + row++; + } else { // This line. + rowMaxHeight = Math.max(rowMaxHeight, childHeight); + } + rowWidth += horizontalSpacing; + } + } + // Account for the last row height. + height += rowMaxHeight; + + // Account for the padding too. + height += getPaddingTop() + getPaddingBottom(); + + // If the tags grouped in one row, set the width to wrap the tags. + if (row == 0) { + width = rowWidth; + width += getPaddingLeft() + getPaddingRight(); + } else {// If the tags grouped exceed one line, set the width to match the parent. + width = widthSize; + } + + setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, + heightMode == MeasureSpec.EXACTLY ? heightSize : height); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int parentLeft = getPaddingLeft(); + final int parentRight = r - l - getPaddingRight(); + final int parentTop = getPaddingTop(); + final int parentBottom = b - t - getPaddingBottom(); + + int childLeft = parentLeft; + int childTop = parentTop; + + int rowMaxHeight = 0; + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + + if (child.getVisibility() != GONE) { + if (childLeft + width > parentRight) { // Next line + childLeft = parentLeft; + childTop += rowMaxHeight + verticalSpacing; + rowMaxHeight = height; + } else { + rowMaxHeight = Math.max(rowMaxHeight, height); + } + child.layout(childLeft, childTop, childLeft + width, childTop + height); + + childLeft += width + horizontalSpacing; + } + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.tags = getTags(); + ss.checkedPosition = getCheckedTagIndex(); + if (getInputTag() != null) { + ss.input = getInputTag().getText().toString(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + setTags(ss.tags); + TagView checkedTagView = getTagAt(ss.checkedPosition); + if (checkedTagView != null) { + checkedTagView.setChecked(true); + } + if (getInputTag() != null) { + getInputTag().setText(ss.input); + } + } + + /** + * Returns the INPUT tag view in this group. + * + * @return the INPUT state tag view or null if not exists + */ + protected TagView getInputTag() { + if (isAppendMode) { + final int inputTagIndex = getChildCount() - 1; + final TagView inputTag = getTagAt(inputTagIndex); + if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) { + return inputTag; + } else { + return null; + } + } else { + return null; + } + } + + /** + * Returns the INPUT state tag in this group. + * + * @return the INPUT state tag view or null if not exists + */ + public String getInputTagText() { + final TagView inputTagView = getInputTag(); + if (inputTagView != null) { + return inputTagView.getText().toString(); + } + return null; + } + + /** + * Return the last NORMAL state tag view in this group. + * + * @return the last NORMAL state tag view or null if not exists + */ + protected TagView getLastNormalTagView() { + final int lastNormalTagIndex = isAppendMode ? getChildCount() - 2 : getChildCount() - 1; + TagView lastNormalTagView = getTagAt(lastNormalTagIndex); + return lastNormalTagView; + } + + /** + * Returns the tag array in group, except the INPUT tag. + * + * @return the tag array. + */ + public String[] getTags() { + final int count = getChildCount(); + final List tagList = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final TagView tagView = getTagAt(i); + if (tagView.mState == TagView.STATE_NORMAL) { + tagList.add(tagView.getText().toString()); + } + } + + return tagList.toArray(new String[tagList.size()]); + } + + /** + * @see #setTags(String...) + */ + public void setTags(List tagList) { + setTags(tagList.toArray(new String[tagList.size()])); + } + + public void setTags2(List tagList) { + setTags(tagList.toArray(new CharSequence[tagList.size()])); + } + + /** + * Set the tags. It will remove all previous tags first. + * + * @param tags the tag list to set. + */ + public void setTags(String... tags) { + removeAllViews(); + for (final String tag : tags) { + appendTag(tag); + } + + if (isAppendMode) { + appendInputTag(); + } + } + + public void setTags(CharSequence... tags) { + removeAllViews(); + for (final CharSequence tag : tags) { + appendTag(tag); + } + + if (isAppendMode) { + appendInputTag(); + } + } + + /** + * Returns the tag view at the specified position in the group. + * + * @param index the position at which to get the tag view from. + * @return the tag view at the specified position or null if the position + * does not exists within this group. + */ + protected TagView getTagAt(int index) { + return (TagView) getChildAt(index); + } + + /** + * Returns the checked tag view in the group. + * + * @return the checked tag view or null if not exists. + */ + protected TagView getCheckedTag() { + final int checkedTagIndex = getCheckedTagIndex(); + if (checkedTagIndex != -1) { + return getTagAt(checkedTagIndex); + } + return null; + } + + /** + * Return the checked tag index. + * + * @return the checked tag index, or -1 if not exists. + */ + protected int getCheckedTagIndex() { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final TagView tag = getTagAt(i); + if (tag.isChecked) { + return i; + } + } + return -1; + } + + /** + * Register a callback to be invoked when this tag group is changed. + * + * @param l the callback that will run + */ + public void setOnTagChangeListener(OnTagChangeListener l) { + mOnTagChangeListener = l; + } + + /** + * @see #appendInputTag(String) + */ + protected void appendInputTag() { + appendInputTag(null); + } + + /** + * Append a INPUT tag to this group. It will throw an exception if there has a previous INPUT tag. + * + * @param tag the tag text. + */ + protected void appendInputTag(String tag) { + final TagView previousInputTag = getInputTag(); + if (previousInputTag != null) { + throw new IllegalStateException("Already has a INPUT tag in group."); + } + + final TagView newInputTag = new TagView(getContext(), TagView.STATE_INPUT, tag); + newInputTag.setOnClickListener(mInternalTagClickListener); + addView(newInputTag); + } + + /** + * Append tag to this group. + * + * @param tag the tag to append. + */ + protected void appendTag(CharSequence tag) { + final TagView newTag = new TagView(getContext(), TagView.STATE_NORMAL, tag); + newTag.setOnClickListener(mInternalTagClickListener); + addView(newTag); + } + + public float dp2px(float dp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, + getResources().getDisplayMetrics()); + } + + public float sp2px(float sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, + getResources().getDisplayMetrics()); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + /** + * Register a callback to be invoked when a tag is clicked. + * + * @param l the callback that will run. + */ + public void setOnTagClickListener(OnTagClickListener l) { + mOnTagClickListener = l; + } + + protected void deleteTag(TagView tagView) { + removeView(tagView); + if (mOnTagChangeListener != null) { + mOnTagChangeListener.onDelete(TagGroup.this, tagView.getText().toString()); + } + } + + /** + * Interface definition for a callback to be invoked when a tag group is changed. + */ + public interface OnTagChangeListener { + /** + * Called when a tag has been appended to the group. + * + * @param tag the appended tag. + */ + void onAppend(TagGroup tagGroup, String tag); + + /** + * Called when a tag has been deleted from the the group. + * + * @param tag the deleted tag. + */ + void onDelete(TagGroup tagGroup, String tag); + } + + /** + * Interface definition for a callback to be invoked when a tag is clicked. + */ + public interface OnTagClickListener { + /** + * Called when a tag has been clicked. + * + * @param tag The tag text of the tag that was clicked. + */ + void onTagClick(String tag); + } + + /** + * Per-child layout information for layouts. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + } + + /** + * For {@link TagGroup} save and restore state. + */ + static class SavedState extends BaseSavedState { + public static final Creator CREATOR = + new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + int tagCount; + String[] tags; + int checkedPosition; + String input; + + public SavedState(Parcel source) { + super(source); + tagCount = source.readInt(); + tags = new String[tagCount]; + source.readStringArray(tags); + checkedPosition = source.readInt(); + input = source.readString(); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + tagCount = tags.length; + dest.writeInt(tagCount); + dest.writeStringArray(tags); + dest.writeInt(checkedPosition); + dest.writeString(input); + } + } + + /** + * The tag view click listener for internal use. + */ + class InternalTagClickListener implements OnClickListener { + @Override + public void onClick(View v) { + final TagView tag = (TagView) v; + if (isAppendMode) { + if (tag.mState == TagView.STATE_INPUT) { + // If the clicked tag is in INPUT state, uncheck the previous checked tag if exists. + final TagView checkedTag = getCheckedTag(); + if (checkedTag != null) { + checkedTag.setChecked(false); + } + } else { + // If the clicked tag is currently checked, delete the tag. + if (tag.isChecked) { + deleteTag(tag); + } else { + // If the clicked tag is unchecked, uncheck the previous checked tag if exists, + // then check the clicked tag. + final TagView checkedTag = getCheckedTag(); + if (checkedTag != null) { + checkedTag.setChecked(false); + } + tag.setChecked(true); + } + } + } else { + if (mOnTagClickListener != null) { + mOnTagClickListener.onTagClick(tag.getText().toString()); + } + } + } + } + + /** + * The tag view which has two states can be either NORMAL or INPUT. + */ + class TagView extends androidx.appcompat.widget.AppCompatTextView { + public static final int STATE_NORMAL = 1; + public static final int STATE_INPUT = 2; + + /** The offset to the text. */ + private static final int CHECKED_MARKER_OFFSET = 3; + + /** The stroke width of the checked marker */ + private static final int CHECKED_MARKER_STROKE_WIDTH = 4; + + /** The current state. */ + private int mState; + + /** Indicates the tag if checked. */ + private boolean isChecked = false; + + /** Indicates the tag if pressed. */ + private boolean isPressed = false; + + private Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private Paint mCheckedMarkerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + /** The rect for the tag's left corner drawing. */ + private RectF mLeftCornerRectF = new RectF(); + + /** The rect for the tag's right corner drawing. */ + private RectF mRightCornerRectF = new RectF(); + + /** The rect for the tag's horizontal blank fill area. */ + private RectF mHorizontalBlankFillRectF = new RectF(); + + /** The rect for the tag's vertical blank fill area. */ + private RectF mVerticalBlankFillRectF = new RectF(); + + /** The rect for the checked mark draw bound. */ + private RectF mCheckedMarkerBound = new RectF(); + + /** Used to detect the touch event. */ + private Rect mOutRect = new Rect(); + + /** The path for draw the tag's outline border. */ + private Path mBorderPath = new Path(); + + /** The path effect provide draw the dash border. */ + private PathEffect mPathEffect = new DashPathEffect(new float[]{10, 5}, 0); + + { + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setStrokeWidth(borderStrokeWidth); + mBackgroundPaint.setStyle(Paint.Style.FILL); + mCheckedMarkerPaint.setStyle(Paint.Style.FILL); + mCheckedMarkerPaint.setStrokeWidth(CHECKED_MARKER_STROKE_WIDTH); + mCheckedMarkerPaint.setColor(checkedMarkerColor); + } + + + public TagView(Context context, final int state, CharSequence text) { + super(context); + setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT)); + + setGravity(Gravity.CENTER); + setText(text); + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + + mState = state; + + setClickable(isAppendMode); + setFocusable(state == STATE_INPUT); + setFocusableInTouchMode(state == STATE_INPUT); + setHint(state == STATE_INPUT ? inputHint : null); + setMovementMethod(state == STATE_INPUT ? ArrowKeyMovementMethod.getInstance() : null); + + // Interrupted long click event to avoid PAUSE popup. + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + return state != STATE_INPUT; + } + }); + + if (state == STATE_INPUT) { + requestFocus(); + + // Handle the ENTER key down. + setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_NULL + && (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER + && event.getAction() == KeyEvent.ACTION_DOWN)) { + if (isInputAvailable()) { + // If the input content is available, end the input and dispatch + // the event, then append a new INPUT state tag. + endInput(); + if (mOnTagChangeListener != null) { + mOnTagChangeListener.onAppend(TagGroup.this, getText().toString()); + } + appendInputTag(); + } + return true; + } + return false; + } + }); + + // Handle the BACKSPACE key down. + setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) { + // If the input content is empty, check or remove the last NORMAL state tag. + if (TextUtils.isEmpty(getText().toString())) { + TagView lastNormalTagView = getLastNormalTagView(); + if (lastNormalTagView != null) { + if (lastNormalTagView.isChecked) { + removeView(lastNormalTagView); + if (mOnTagChangeListener != null) { + mOnTagChangeListener.onDelete(TagGroup.this, lastNormalTagView.getText().toString()); + } + } else { + final TagView checkedTagView = getCheckedTag(); + if (checkedTagView != null) { + checkedTagView.setChecked(false); + } + lastNormalTagView.setChecked(true); + } + return true; + } + } + } + return false; + } + }); + + // Handle the INPUT tag content changed. + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // When the INPUT state tag changed, uncheck the checked tag if exists. + final TagView checkedTagView = getCheckedTag(); + if (checkedTagView != null) { + checkedTagView.setChecked(false); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + invalidatePaint(); + } + + /** + * Set whether this tag view is in the checked state. + * + * @param checked true is checked, false otherwise + */ + public void setChecked(boolean checked) { + isChecked = checked; + // Make the checked mark drawing region. + setPadding(horizontalPadding, + verticalPadding, + isChecked ? (int) (horizontalPadding + getHeight() / 2.5f + CHECKED_MARKER_OFFSET) + : horizontalPadding, + verticalPadding); + invalidatePaint(); + } + + /** + * Call this method to end this tag's INPUT state. + */ + public void endInput() { + // Make the view not focusable. + setFocusable(false); + setFocusableInTouchMode(false); + // Set the hint empty, make the TextView measure correctly. + setHint(null); + // Take away the cursor. + setMovementMethod(null); + + mState = STATE_NORMAL; + invalidatePaint(); + requestLayout(); + } + + @Override + protected boolean getDefaultEditable() { + return true; + } + + /** + * Indicates whether the input content is available. + * + * @return True if the input content is available, false otherwise. + */ + public boolean isInputAvailable() { + return getText() != null && getText().length() > 0; + } + + private void invalidatePaint() { + if (isAppendMode) { + if (mState == STATE_INPUT) { + mBorderPaint.setColor(dashBorderColor); + mBorderPaint.setPathEffect(mPathEffect); + mBackgroundPaint.setColor(backgroundColor); + setHintTextColor(inputHintColor); + setTextColor(inputTextColor); + } else { + mBorderPaint.setPathEffect(null); + if (isChecked) { + mBorderPaint.setColor(checkedBorderColor); + mBackgroundPaint.setColor(checkedBackgroundColor); + setTextColor(checkedTextColor); + } else { + mBorderPaint.setColor(borderColor); + mBackgroundPaint.setColor(backgroundColor); + setTextColor(textColor); + } + } + } else { + mBorderPaint.setColor(borderColor); + mBackgroundPaint.setColor(backgroundColor); + setTextColor(textColor); + } + + if (isPressed) { + mBackgroundPaint.setColor(pressedBackgroundColor); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawArc(mLeftCornerRectF, -180, 90, true, mBackgroundPaint); + canvas.drawArc(mLeftCornerRectF, -270, 90, true, mBackgroundPaint); + canvas.drawArc(mRightCornerRectF, -90, 90, true, mBackgroundPaint); + canvas.drawArc(mRightCornerRectF, 0, 90, true, mBackgroundPaint); + canvas.drawRect(mHorizontalBlankFillRectF, mBackgroundPaint); + canvas.drawRect(mVerticalBlankFillRectF, mBackgroundPaint); + + if (isChecked) { + canvas.save(); + canvas.rotate(45, mCheckedMarkerBound.centerX(), mCheckedMarkerBound.centerY()); + canvas.drawLine(mCheckedMarkerBound.left, mCheckedMarkerBound.centerY(), + mCheckedMarkerBound.right, mCheckedMarkerBound.centerY(), mCheckedMarkerPaint); + canvas.drawLine(mCheckedMarkerBound.centerX(), mCheckedMarkerBound.top, + mCheckedMarkerBound.centerX(), mCheckedMarkerBound.bottom, mCheckedMarkerPaint); + canvas.restore(); + } + canvas.drawPath(mBorderPath, mBorderPaint); + super.onDraw(canvas); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + int left = (int) borderStrokeWidth; + int top = (int) borderStrokeWidth; + int right = (int) (left + w - borderStrokeWidth * 2); + int bottom = (int) (top + h - borderStrokeWidth * 2); + + int d = bottom - top; + + mLeftCornerRectF.set(left, top, left + d, top + d); + mRightCornerRectF.set(right - d, top, right, top + d); + + mBorderPath.reset(); + mBorderPath.addArc(mLeftCornerRectF, -180, 90); + mBorderPath.addArc(mLeftCornerRectF, -270, 90); + mBorderPath.addArc(mRightCornerRectF, -90, 90); + mBorderPath.addArc(mRightCornerRectF, 0, 90); + + int l = (int) (d / 2.0f); + mBorderPath.moveTo(left + l, top); + mBorderPath.lineTo(right - l, top); + + mBorderPath.moveTo(left + l, bottom); + mBorderPath.lineTo(right - l, bottom); + + mBorderPath.moveTo(left, top + l); + mBorderPath.lineTo(left, bottom - l); + + mBorderPath.moveTo(right, top + l); + mBorderPath.lineTo(right, bottom - l); + + mHorizontalBlankFillRectF.set(left, top + l, right, bottom - l); + mVerticalBlankFillRectF.set(left + l, top, right - l, bottom); + + int m = (int) (h / 2.5f); + h = bottom - top; + mCheckedMarkerBound.set(right - m - horizontalPadding + CHECKED_MARKER_OFFSET, + top + h / 2 - m / 2, + right - horizontalPadding + CHECKED_MARKER_OFFSET, + bottom - h / 2 + m / 2); + + // Ensure the checked mark drawing region is correct across screen orientation changes. + if (isChecked) { + setPadding(horizontalPadding, + verticalPadding, + (int) (horizontalPadding + h / 2.5f + CHECKED_MARKER_OFFSET), + verticalPadding); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mState == STATE_INPUT) { + // The INPUT tag doesn't change background color on the touch event. + return super.onTouchEvent(event); + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + getDrawingRect(mOutRect); + isPressed = true; + invalidatePaint(); + invalidate(); + break; + } + case MotionEvent.ACTION_MOVE: { + if (!mOutRect.contains((int) event.getX(), (int) event.getY())) { + isPressed = false; + invalidatePaint(); + invalidate(); + } + break; + } + case MotionEvent.ACTION_UP: { + isPressed = false; + invalidatePaint(); + invalidate(); + break; + } + } + return super.onTouchEvent(event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return new ZanyInputConnection(super.onCreateInputConnection(outAttrs), true); + } + + /** + * Solve edit text delete(backspace) key detect, see + * Android: Backspace in WebView/BaseInputConnection + */ + private class ZanyInputConnection extends InputConnectionWrapper { + public ZanyInputConnection(InputConnection target, boolean mutable) { + super(target, mutable); + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + // magic: in latest Android, deleteSurroundingText(1, 0) will be called for backspace + if (beforeLength == 1 && afterLength == 0) { + // backspace + return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) + && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); + } + return super.deleteSurroundingText(beforeLength, afterLength); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_book.xml b/app/src/main/res/layout/activity_search_book.xml index 5c5dd2b..7ce1983 100644 --- a/app/src/main/res/layout/activity_search_book.xml +++ b/app/src/main/res/layout/activity_search_book.xml @@ -96,7 +96,7 @@ android:orientation="vertical" android:padding="5dp"> - - + + + + + + + + + Add Tag + + + + + + + diff --git a/app/version_code.properties b/app/version_code.properties index 8520ba5..745c4da 100644 --- a/app/version_code.properties +++ b/app/version_code.properties @@ -1,2 +1,2 @@ -#Tue Jun 15 19:45:01 CST 2021 -VERSION_CODE=212 +#Tue Jun 15 20:58:21 CST 2021 +VERSION_CODE=213