Merge pull request #8556 from vector-im/feature/bma/noJcenter

Feature/bma/no jcenter
This commit is contained in:
Benoit Marty 2023-06-27 17:04:59 +02:00 committed by GitHub
commit 2b4b5f05eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 4214 additions and 46 deletions

View File

@ -112,16 +112,6 @@ allprojects {
groups.google.group.each { includeGroup it }
}
}
//noinspection JcenterRepositoryObsolete
// Do not use `jcenter`, it prevents Dependabot from working properly
maven {
url 'https://jcenter.bintray.com'
content {
groups.jcenter.regex.each { includeGroupByRegex it }
groups.jcenter.group.each { includeGroup it }
}
}
maven {
url 'https://s01.oss.sonatype.org/content/repositories/snapshots'
content {
@ -129,7 +119,6 @@ allprojects {
groups.mavenSnapshots.group.each { includeGroup it }
}
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {

1
changelog.d/8556.misc Normal file
View File

@ -0,0 +1 @@
Include some source code in our project to remove our dependency to artifact hosted by bintray (Jcenter).

View File

@ -115,6 +115,7 @@ ext.groups = [
'com.linkedin.dexmaker',
'com.mapbox.mapboxsdk',
'com.nulab-inc',
'com.otaliastudios',
'com.otaliastudios.opengl',
'com.parse.bolts',
'com.pinterest',
@ -234,18 +235,4 @@ ext.groups = [
'xml-apis',
]
],
jcenter : [
regex: [
],
group: [
'com.amulyakhare',
'com.otaliastudios',
'com.yqritc',
// https://github.com/cmelchior/realmfieldnameshelper/issues/42
'dk.ilios',
'im.dlg',
'me.dm7.barcodescanner',
'me.gujun.android',
]
]
]

View File

@ -0,0 +1,32 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "com.otaliastudios.autocomplete"
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
}
compileOptions {
sourceCompatibility versions.sourceCompat
targetCompatibility versions.targetCompat
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation libs.androidx.recyclerview
}
afterEvaluate {
tasks.findAll { it.name.startsWith("lint") }.each {
it.enabled = false
}
}

View File

@ -0,0 +1,434 @@
package com.otaliastudios.autocomplete;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextWatcher;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.PopupWindow;
import androidx.annotation.NonNull;
/**
* Entry point for adding Autocomplete behavior to a {@link EditText}.
*
* You can construct a {@code Autocomplete} using the builder provided by {@link Autocomplete#on(EditText)}.
* Building is enough, but you can hold a reference to this class to call its public methods.
*
* Requires:
* - {@link EditText}: this is both the anchor for the popup, and the source of text events that we listen to
* - {@link AutocompletePresenter}: this presents items in the popup window. See class for more info.
* - {@link AutocompleteCallback}: if specified, this listens to click events and visibility changes
* - {@link AutocompletePolicy}: if specified, this controls how and when to show the popup based on text events
* If not, this defaults to {@link SimplePolicy}: shows the popup when text.length() bigger than 0.
*/
public final class Autocomplete<T> implements TextWatcher, SpanWatcher {
private final static String TAG = Autocomplete.class.getSimpleName();
private final static boolean DEBUG = false;
private static void log(String log) {
if (DEBUG) Log.e(TAG, log);
}
/**
* Builder for building {@link Autocomplete}.
* The only mandatory item is a presenter, {@link #with(AutocompletePresenter)}.
*
* @param <T> the data model
*/
public final static class Builder<T> {
private EditText source;
private AutocompletePresenter<T> presenter;
private AutocompletePolicy policy;
private AutocompleteCallback<T> callback;
private Drawable backgroundDrawable;
private float elevationDp = 6;
private Builder(EditText source) {
this.source = source;
}
/**
* Registers the {@link AutocompletePresenter} to be used, responsible for showing
* items. See the class for info.
*
* @param presenter desired presenter
* @return this for chaining
*/
public Builder<T> with(AutocompletePresenter<T> presenter) {
this.presenter = presenter;
return this;
}
/**
* Registers the {@link AutocompleteCallback} to be used, responsible for listening to
* clicks provided by the presenter, and visibility changes.
*
* @param callback desired callback
* @return this for chaining
*/
public Builder<T> with(AutocompleteCallback<T> callback) {
this.callback = callback;
return this;
}
/**
* Registers the {@link AutocompletePolicy} to be used, responsible for showing / dismissing
* the popup when certain events happen (e.g. certain characters are typed).
*
* @param policy desired policy
* @return this for chaining
*/
public Builder<T> with(AutocompletePolicy policy) {
this.policy = policy;
return this;
}
/**
* Sets a background drawable for the popup.
*
* @param backgroundDrawable drawable
* @return this for chaining
*/
public Builder<T> with(Drawable backgroundDrawable) {
this.backgroundDrawable = backgroundDrawable;
return this;
}
/**
* Sets elevation for the popup. Defaults to 6 dp.
*
* @param elevationDp popup elevation, in DP
* @return this for chaning.
*/
public Builder<T> with(float elevationDp) {
this.elevationDp = elevationDp;
return this;
}
/**
* Builds an Autocomplete instance. This is enough for autocomplete to be set up,
* but you can hold a reference to the object and call its public methods.
*
* @return an Autocomplete instance, if you need it
*
* @throws RuntimeException if either EditText or the presenter are null
*/
public Autocomplete<T> build() {
if (source == null) throw new RuntimeException("Autocomplete needs a source!");
if (presenter == null) throw new RuntimeException("Autocomplete needs a presenter!");
if (policy == null) policy = new SimplePolicy();
return new Autocomplete<T>(this);
}
private void clear() {
source = null;
presenter = null;
callback = null;
policy = null;
backgroundDrawable = null;
elevationDp = 6;
}
}
/**
* Entry point for building autocomplete on a certain {@link EditText}.
* @param anchor the anchor for the popup, and the source of text events
* @param <T> your data model
* @return a Builder for set up
*/
public static <T> Builder<T> on(EditText anchor) {
return new Builder<T>(anchor);
}
private AutocompletePolicy policy;
private AutocompletePopup popup;
private AutocompletePresenter<T> presenter;
private AutocompleteCallback<T> callback;
private EditText source;
private boolean block;
private boolean disabled;
private boolean openBefore;
private String lastQuery = "null";
private Autocomplete(Builder<T> builder) {
policy = builder.policy;
presenter = builder.presenter;
callback = builder.callback;
source = builder.source;
// Set up popup
popup = new AutocompletePopup(source.getContext());
popup.setAnchorView(source);
popup.setGravity(Gravity.START);
popup.setModal(false);
popup.setBackgroundDrawable(builder.backgroundDrawable);
popup.setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, builder.elevationDp,
source.getContext().getResources().getDisplayMetrics()));
// popup dimensions
AutocompletePresenter.PopupDimensions dim = this.presenter.getPopupDimensions();
popup.setWidth(dim.width);
popup.setHeight(dim.height);
popup.setMaxWidth(dim.maxWidth);
popup.setMaxHeight(dim.maxHeight);
// Fire visibility events
popup.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
lastQuery = "null";
if (callback != null) callback.onPopupVisibilityChanged(false);
boolean saved = block;
block = true;
policy.onDismiss(source.getText());
block = saved;
presenter.hideView();
}
});
// Set up source
source.getText().setSpan(this, 0, source.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
source.addTextChangedListener(this);
// Set up presenter
presenter.registerClickProvider(new AutocompletePresenter.ClickProvider<T>() {
@Override
public void click(@NonNull T item) {
AutocompleteCallback<T> callback = Autocomplete.this.callback;
EditText edit = Autocomplete.this.source;
if (callback == null) return;
boolean saved = block;
block = true;
boolean dismiss = callback.onPopupItemClicked(edit.getText(), item);
if (dismiss) dismissPopup();
block = saved;
}
});
builder.clear();
}
/**
* Controls how the popup operates with an input method.
*
* If the popup is showing, calling this method will take effect only
* the next time the popup is shown.
*
* @param mode a {@link PopupWindow} input method mode
*/
public void setInputMethodMode(int mode) {
popup.setInputMethodMode(mode);
}
/**
* Sets the operating mode for the soft input area.
*
* @param mode The desired mode, see {@link WindowManager.LayoutParams#softInputMode}
*/
public void setSoftInputMode(int mode) {
popup.setSoftInputMode(mode);
}
/**
* Shows the popup with the given query.
* There is rarely need to call this externally: it is already triggered by events on the anchor.
* To control when this is called, provide a good implementation of {@link AutocompletePolicy}.
*
* @param query query text.
*/
public void showPopup(@NonNull CharSequence query) {
if (isPopupShowing() && lastQuery.equals(query.toString())) return;
lastQuery = query.toString();
log("showPopup: called with filter "+query);
if (!isPopupShowing()) {
log("showPopup: showing");
presenter.registerDataSetObserver(new Observer()); // Calling new to avoid leaking... maybe...
popup.setView(presenter.getView());
presenter.showView();
popup.show();
if (callback != null) callback.onPopupVisibilityChanged(true);
}
log("showPopup: popup should be showing... "+isPopupShowing());
presenter.onQuery(query);
}
/**
* Dismisses the popup, if showing.
* There is rarely need to call this externally: it is already triggered by events on the anchor.
* To control when this is called, provide a good implementation of {@link AutocompletePolicy}.
*/
public void dismissPopup() {
if (isPopupShowing()) {
popup.dismiss();
}
}
/**
* Returns true if the popup is showing.
* @return whether the popup is currently showing
*/
public boolean isPopupShowing() {
return this.popup.isShowing();
}
/**
* Switch to control the autocomplete behavior. When disabled, no popup is shown.
* This is useful if you want to do runtime edits to the anchor text, without triggering
* the popup.
*
* @param enabled whether to enable autocompletion
*/
public void setEnabled(boolean enabled) {
disabled = !enabled;
}
/**
* Sets the gravity for the popup. Basically only {@link Gravity#START} and {@link Gravity#END}
* do work.
*
* @param gravity gravity for the popup
*/
public void setGravity(int gravity) {
popup.setGravity(gravity);
}
/**
* Controls the vertical offset of the popup from the EditText anchor.
*
* @param offset offset in pixels.
*/
public void setOffsetFromAnchor(int offset) { popup.setVerticalOffset(offset); }
/**
* Controls whether the popup should listen to clicks outside its boundaries.
*
* @param outsideTouchable true to listen to outside clicks
*/
public void setOutsideTouchable(boolean outsideTouchable) { popup.setOutsideTouchable(outsideTouchable); }
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (block || disabled) return;
openBefore = isPopupShowing();
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (block || disabled) return;
if (openBefore && !isPopupShowing()) {
return; // Copied from somewhere.
}
if (!(s instanceof Spannable)) {
source.setText(new SpannableString(s));
return;
}
Spannable sp = (Spannable) s;
int cursor = source.getSelectionEnd();
log("onTextChanged: cursor end position is "+cursor);
if (cursor == -1) { // No cursor present.
dismissPopup(); return;
}
if (cursor != source.getSelectionStart()) {
// Not sure about this. We should have no problems dealing with multi selections,
// we just take the end...
// dismissPopup(); return;
}
boolean b = block;
block = true; // policy might add spans or other stuff.
if (isPopupShowing() && policy.shouldDismissPopup(sp, cursor)) {
log("onTextChanged: dismissing");
dismissPopup();
} else if (isPopupShowing() || policy.shouldShowPopup(sp, cursor)) {
// LOG.now("onTextChanged: updating with filter "+policy.getQuery(sp));
showPopup(policy.getQuery(sp));
}
block = b;
}
@Override
public void afterTextChanged(Editable s) {}
@Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {}
@Override
public void onSpanRemoved(Spannable text, Object what, int start, int end) {}
@Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
if (disabled || block) return;
if (what == Selection.SELECTION_END) {
// Selection end changed from ostart to nstart. Trigger a check.
log("onSpanChanged: selection end moved from "+ostart+" to "+nstart);
log("onSpanChanged: block is "+block);
boolean b = block;
block = true;
if (!isPopupShowing() && policy.shouldShowPopup(text, nstart)) {
showPopup(policy.getQuery(text));
}
block = b;
}
}
private class Observer extends DataSetObserver implements Runnable {
private Handler ui = new Handler(Looper.getMainLooper());
@Override
public void onChanged() {
// ??? Not sure this is needed...
ui.post(this);
}
@Override
public void run() {
if (isPopupShowing()) {
// Call show again to revisit width and height.
popup.show();
}
}
}
/**
* A very simple {@link AutocompletePolicy} implementation.
* Popup is shown when text length is bigger than 0, and hidden when text is empty.
* The query string is the whole text.
*/
public static class SimplePolicy implements AutocompletePolicy {
@Override
public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) {
return text.length() > 0;
}
@Override
public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) {
return text.length() == 0;
}
@NonNull
@Override
public CharSequence getQuery(@NonNull Spannable text) {
return text;
}
@Override
public void onDismiss(@NonNull Spannable text) {}
}
}

View File

@ -0,0 +1,29 @@
package com.otaliastudios.autocomplete;
import android.text.Editable;
import androidx.annotation.NonNull;
/**
* Optional callback to be passed to {@link Autocomplete.Builder}.
*/
public interface AutocompleteCallback<T> {
/**
* Called when an item inside your list is clicked.
* This works if your presenter has dispatched a click event.
* At this point you can edit the text, e.g. {@code editable.append(item.toString())}.
*
* @param editable editable text that you can work on
* @param item item that was clicked
* @return true if the action is valid and the popup can be dismissed
*/
boolean onPopupItemClicked(@NonNull Editable editable, @NonNull T item);
/**
* Called when popup visibility state changes.
*
* @param shown true if the popup was just shown, false if it was just hidden
*/
void onPopupVisibilityChanged(boolean shown);
}

View File

@ -0,0 +1,64 @@
package com.otaliastudios.autocomplete;
import android.text.Spannable;
import androidx.annotation.NonNull;
/**
* This interface controls when to show or hide the popup window, and, in the first case,
* what text should be passed to the popup {@link AutocompletePresenter}.
*
* @see Autocomplete.SimplePolicy for the simplest possible implementation
*/
public interface AutocompletePolicy {
/**
* Called to understand whether the popup should be shown. Some naive examples:
* - Show when there's text: {@code return text.length() > 0}
* - Show when last char is @: {@code return text.getCharAt(text.length()-1) == '@'}
*
* @param text current text, along with its Spans
* @param cursorPos the position of the cursor
* @return true if popup should be shown
*/
boolean shouldShowPopup(@NonNull Spannable text, int cursorPos);
/**
* Called to understand whether a currently shown popup should be closed, maybe
* because text is invalid. A reasonable implementation is
* {@code return !shouldShowPopup(text, cursorPos)}.
*
* However this is defined so you can add or clear spans.
*
* @param text current text, along with its Spans
* @param cursorPos the position of the cursor
* @return true if popup should be hidden
*/
boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos);
/**
* Called to understand which query should be passed to {@link AutocompletePresenter}
* for a showing popup. If this is called, {@link #shouldShowPopup(Spannable, int)} just returned
* true, or {@link #shouldDismissPopup(Spannable, int)} just returned false.
*
* This is useful to understand which part of the text should be passed to presenters.
* For example, user might have typed '@john' to select a username, but you just want to
* search for 'john'.
*
* For more complex cases, you can add inclusive Spans in {@link #shouldShowPopup(Spannable, int)},
* and get the span position here.
*
* @param text current text, along with its Spans
* @return the query for presenter
*/
@NonNull
CharSequence getQuery(@NonNull Spannable text);
/**
* Called when popup is dismissed. This can be used, for instance, to clear custom Spans
* from the text.
*
* @param text text at the moment of dismissing
*/
void onDismiss(@NonNull Spannable text);
}

View File

@ -0,0 +1,521 @@
package com.otaliastudios.autocomplete;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.PopupWindow;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.core.view.ViewCompat;
import androidx.core.widget.PopupWindowCompat;
/**
* A simplified version of andriod.widget.ListPopupWindow, which is the class used by
* AutocompleteTextView.
*
* Other than being simplified, this deals with Views rather than ListViews, so the content
* can be whatever. Lots of logic (clicks, selections etc.) has been removed because we manage that
* in {@link AutocompletePresenter}.
*
*/
class AutocompletePopup {
private Context mContext;
private ViewGroup mView;
private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mMaxHeight = Integer.MAX_VALUE;
private int mMaxWidth = Integer.MAX_VALUE;
private int mUserMaxHeight = Integer.MAX_VALUE;
private int mUserMaxWidth = Integer.MAX_VALUE;
private int mHorizontalOffset = 0;
private int mVerticalOffset = 0;
private boolean mVerticalOffsetSet;
private int mGravity = Gravity.NO_GRAVITY;
private boolean mAlwaysVisible = false;
private boolean mOutsideTouchable = true;
private View mAnchorView;
private final Rect mTempRect = new Rect();
private boolean mModal;
private PopupWindow mPopup;
/**
* Create a new, empty popup window capable of displaying items from a ListAdapter.
* Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
*
* @param context Context used for contained views.
*/
AutocompletePopup(@NonNull Context context) {
super();
mContext = context;
mPopup = new PopupWindow(context);
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
}
/**
* Set whether this window should be modal when shown.
*
* <p>If a popup window is modal, it will receive all touch and key input.
* If the user touches outside the popup window's content area the popup window
* will be dismissed.
* @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
*/
@SuppressWarnings("SameParameterValue")
void setModal(boolean modal) {
mModal = modal;
mPopup.setFocusable(modal);
}
/**
* Returns whether the popup window will be modal when shown.
* @return {@code true} if the popup window will be modal, {@code false} otherwise.
*/
@SuppressWarnings("unused")
boolean isModal() {
return mModal;
}
void setElevation(float elevationPx) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mPopup.setElevation(elevationPx);
}
/**
* Sets whether the drop-down should remain visible under certain conditions.
*
* The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
* of the size or content of the list. {@link #getBackground()} will fill any space
* that is not used by the list.
* @param dropDownAlwaysVisible Whether to keep the drop-down visible.
*
*/
@SuppressWarnings("unused")
void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
mAlwaysVisible = dropDownAlwaysVisible;
}
/**
* @return Whether the drop-down is visible under special conditions.
*/
@SuppressWarnings("unused")
boolean isDropDownAlwaysVisible() {
return mAlwaysVisible;
}
void setOutsideTouchable(boolean outsideTouchable) {
mOutsideTouchable = outsideTouchable;
}
@SuppressWarnings("WeakerAccess")
boolean isOutsideTouchable() {
return mOutsideTouchable && !mAlwaysVisible;
}
/**
* Sets the operating mode for the soft input area.
* @param mode The desired mode, see
* {@link android.view.WindowManager.LayoutParams#softInputMode}
* for the full list
* @see android.view.WindowManager.LayoutParams#softInputMode
* @see #getSoftInputMode()
*/
void setSoftInputMode(int mode) {
mPopup.setSoftInputMode(mode);
}
/**
* Returns the current value in {@link #setSoftInputMode(int)}.
* @see #setSoftInputMode(int)
* @see android.view.WindowManager.LayoutParams#softInputMode
*/
@SuppressWarnings({"WeakerAccess", "unused"})
int getSoftInputMode() {
return mPopup.getSoftInputMode();
}
/**
* @return The background drawable for the popup window.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
@Nullable
Drawable getBackground() {
return mPopup.getBackground();
}
/**
* Sets a drawable to be the background for the popup window.
* @param d A drawable to set as the background.
*/
void setBackgroundDrawable(@Nullable Drawable d) {
mPopup.setBackgroundDrawable(d);
}
/**
* Set an animation style to use when the popup window is shown or dismissed.
* @param animationStyle Animation style to use.
*/
@SuppressWarnings("unused")
void setAnimationStyle(@StyleRes int animationStyle) {
mPopup.setAnimationStyle(animationStyle);
}
/**
* Returns the animation style that will be used when the popup window is
* shown or dismissed.
* @return Animation style that will be used.
*/
@SuppressWarnings("unused")
@StyleRes
int getAnimationStyle() {
return mPopup.getAnimationStyle();
}
/**
* Returns the view that will be used to anchor this popup.
* @return The popup's anchor view
*/
@SuppressWarnings("WeakerAccess")
View getAnchorView() {
return mAnchorView;
}
/**
* Sets the popup's anchor view. This popup will always be positioned relative to
* the anchor view when shown.
* @param anchor The view to use as an anchor.
*/
void setAnchorView(@NonNull View anchor) {
mAnchorView = anchor;
}
/**
* Set the horizontal offset of this popup from its anchor view in pixels.
* @param offset The horizontal offset of the popup from its anchor.
*/
@SuppressWarnings("unused")
void setHorizontalOffset(int offset) {
mHorizontalOffset = offset;
}
/**
* Set the vertical offset of this popup from its anchor view in pixels.
* @param offset The vertical offset of the popup from its anchor.
*/
void setVerticalOffset(int offset) {
mVerticalOffset = offset;
mVerticalOffsetSet = true;
}
/**
* Set the gravity of the dropdown list. This is commonly used to
* set gravity to START or END for alignment with the anchor.
* @param gravity Gravity value to use
*/
void setGravity(int gravity) {
mGravity = gravity;
}
/**
* @return The width of the popup window in pixels.
*/
@SuppressWarnings("unused")
int getWidth() {
return mWidth;
}
/**
* Sets the width of the popup window in pixels. Can also be MATCH_PARENT
* or WRAP_CONTENT.
* @param width Width of the popup window.
*/
void setWidth(int width) {
mWidth = width;
}
/**
* Sets the width of the popup window by the size of its content. The final width may be
* larger to accommodate styled window dressing.
* @param width Desired width of content in pixels.
*/
@SuppressWarnings("unused")
void setContentWidth(int width) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
width += mTempRect.left + mTempRect.right;
}
setWidth(width);
}
void setMaxWidth(int width) {
if (width > 0) {
mUserMaxWidth = width;
}
}
/**
* @return The height of the popup window in pixels.
*/
@SuppressWarnings("unused")
int getHeight() {
return mHeight;
}
/**
* Sets the height of the popup window in pixels. Can also be MATCH_PARENT.
* @param height Height of the popup window.
*/
void setHeight(int height) {
mHeight = height;
}
/**
* Sets the height of the popup window by the size of its content. The final height may be
* larger to accommodate styled window dressing.
* @param height Desired height of content in pixels.
*/
@SuppressWarnings("unused")
void setContentHeight(int height) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
height += mTempRect.top + mTempRect.bottom;
}
setHeight(height);
}
void setMaxHeight(int height) {
if (height > 0) {
mUserMaxHeight = height;
}
}
void setOnDismissListener(PopupWindow.OnDismissListener listener) {
mPopup.setOnDismissListener(listener);
}
/**
* Show the popup list. If the list is already showing, this method
* will recalculate the popup's size and position.
*/
void show() {
if (!ViewCompat.isAttachedToWindow(getAnchorView())) return;
int height = buildDropDown();
final boolean noInputMethod = isInputMethodNotNeeded();
int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
PopupWindowCompat.setWindowLayoutType(mPopup, mDropDownWindowLayoutType);
if (mPopup.isShowing()) {
// First pass for this special case, don't know why.
if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
int tempWidth = mWidth == ViewGroup.LayoutParams.MATCH_PARENT ? ViewGroup.LayoutParams.MATCH_PARENT : 0;
if (noInputMethod) {
mPopup.setWidth(tempWidth);
mPopup.setHeight(0);
} else {
mPopup.setWidth(tempWidth);
mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
}
}
// The call to PopupWindow's update method below can accept -1
// for any value you do not want to update.
// Width.
int widthSpec;
if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
widthSpec = -1;
} else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
widthSpec = getAnchorView().getWidth();
} else {
widthSpec = mWidth;
}
widthSpec = Math.min(widthSpec, mMaxWidth);
widthSpec = (widthSpec < 0) ? - 1 : widthSpec;
// Height.
int heightSpec;
if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
} else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
heightSpec = height;
} else {
heightSpec = mHeight;
}
heightSpec = Math.min(heightSpec, mMaxHeight);
heightSpec = (heightSpec < 0) ? - 1 : heightSpec;
// Update.
mPopup.setOutsideTouchable(isOutsideTouchable());
if (heightSpec == 0) {
dismiss();
} else {
mPopup.update(getAnchorView(), mHorizontalOffset, mVerticalOffset, widthSpec, heightSpec);
}
} else {
int widthSpec;
if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
} else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
widthSpec = getAnchorView().getWidth();
} else {
widthSpec = mWidth;
}
widthSpec = Math.min(widthSpec, mMaxWidth);
int heightSpec;
if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
} else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
heightSpec = height;
} else {
heightSpec = mHeight;
}
heightSpec = Math.min(heightSpec, mMaxHeight);
// Set width and height.
mPopup.setWidth(widthSpec);
mPopup.setHeight(heightSpec);
mPopup.setClippingEnabled(true);
// use outside touchable to dismiss drop down when touching outside of it, so
// only set this if the dropdown is not always visible
mPopup.setOutsideTouchable(isOutsideTouchable());
PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mHorizontalOffset, mVerticalOffset, mGravity);
}
}
/**
* Dismiss the popup window.
*/
void dismiss() {
mPopup.dismiss();
mPopup.setContentView(null);
mView = null;
}
/**
* Control how the popup operates with an input method: one of
* INPUT_METHOD_FROM_FOCUSABLE, INPUT_METHOD_NEEDED,
* or INPUT_METHOD_NOT_NEEDED.
*
* <p>If the popup is showing, calling this method will take effect only
* the next time the popup is shown or through a manual call to the {@link #show()}
* method.</p>
*
* @see #show()
*/
void setInputMethodMode(int mode) {
mPopup.setInputMethodMode(mode);
}
/**
* @return {@code true} if the popup is currently showing, {@code false} otherwise.
*/
boolean isShowing() {
return mPopup.isShowing();
}
/**
* @return {@code true} if this popup is configured to assume the user does not need
* to interact with the IME while it is showing, {@code false} otherwise.
*/
@SuppressWarnings("WeakerAccess")
boolean isInputMethodNotNeeded() {
return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
}
void setView(ViewGroup view) {
mView = view;
mView.setFocusable(true);
mView.setFocusableInTouchMode(true);
ViewGroup dropDownView = mView;
mPopup.setContentView(dropDownView);
ViewGroup.LayoutParams params = mView.getLayoutParams();
if (params != null) {
if (params.height > 0) setHeight(params.height);
if (params.width > 0) setWidth(params.width);
}
}
/**
* <p>Builds the popup window's content and returns the height the popup
* should have. Returns -1 when the content already exists.</p>
*
* @return the content's wrap content height or -1 if content already exists
*/
private int buildDropDown() {
int otherHeights = 0;
// getMaxAvailableHeight() subtracts the padding, so we put it back
// to get the available height for the whole window.
final int paddingVert;
final int paddingHoriz;
final Drawable background = mPopup.getBackground();
if (background != null) {
background.getPadding(mTempRect);
paddingVert = mTempRect.top + mTempRect.bottom;
paddingHoriz = mTempRect.left + mTempRect.right;
// If we don't have an explicit vertical offset, determine one from
// the window background so that content will line up.
if (!mVerticalOffsetSet) {
mVerticalOffset = -mTempRect.top;
}
} else {
mTempRect.setEmpty();
paddingVert = 0;
paddingHoriz = 0;
}
// Redefine dimensions taking into account maxWidth and maxHeight.
final boolean ignoreBottomDecorations = mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
final int maxContentHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?
mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset, ignoreBottomDecorations) :
mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset);
final int maxContentWidth = mContext.getResources().getDisplayMetrics().widthPixels - paddingHoriz;
mMaxHeight = Math.min(maxContentHeight + paddingVert, mUserMaxHeight);
mMaxWidth = Math.min(maxContentWidth + paddingHoriz, mUserMaxWidth);
// if (mHeight > 0) mHeight = Math.min(mHeight, maxContentHeight);
// if (mWidth > 0) mWidth = Math.min(mWidth, maxContentWidth);
if (mAlwaysVisible || mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
return mMaxHeight;
}
final int childWidthSpec;
switch (mWidth) {
case ViewGroup.LayoutParams.WRAP_CONTENT:
childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.AT_MOST); break;
case ViewGroup.LayoutParams.MATCH_PARENT:
childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.EXACTLY); break;
default:
//noinspection Range
childWidthSpec = View.MeasureSpec.makeMeasureSpec(mWidth, View.MeasureSpec.EXACTLY); break;
}
// Add padding only if the list has items in it, that way we don't show
// the popup if it is not needed. For this reason, we measure as wrap_content.
mView.measure(childWidthSpec, View.MeasureSpec.makeMeasureSpec(maxContentHeight, View.MeasureSpec.AT_MOST));
final int viewHeight = mView.getMeasuredHeight();
if (viewHeight > 0) {
otherHeights += paddingVert + mView.getPaddingTop() + mView.getPaddingBottom();
}
return Math.min(viewHeight + otherHeights, mMaxHeight);
}
}

View File

@ -0,0 +1,129 @@
package com.otaliastudios.autocomplete;
import android.content.Context;
import android.database.DataSetObserver;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Base class for presenting items inside a popup. This is abstract and must be implemented.
*
* Most important methods are {@link #getView()} and {@link #onQuery(CharSequence)}.
*/
public abstract class AutocompletePresenter<T> {
private Context context;
private boolean isShowing;
@SuppressWarnings("WeakerAccess")
public AutocompletePresenter(@NonNull Context context) {
this.context = context;
}
/**
* At this point the presenter is passed the {@link ClickProvider}.
* The contract is that {@link ClickProvider#click(Object)} must be called when a list item
* is clicked. This ensure that the autocomplete callback will receive the event.
*
* @param provider a click provider for this presenter.
*/
protected void registerClickProvider(ClickProvider<T> provider) {
}
/**
* Useful if you wish to change width/height based on content height.
* The contract is to call {@link DataSetObserver#onChanged()} when your view has
* changes.
*
* This is called after {@link #getView()}.
*
* @param observer the observer.
*/
protected void registerDataSetObserver(@NonNull DataSetObserver observer) {}
/**
* Called each time the popup is shown. You are meant to inflate the view here.
* You can get a LayoutInflater using {@link #getContext()}.
*
* @return a ViewGroup for the popup
*/
@NonNull
protected abstract ViewGroup getView();
/**
* Provide the {@link PopupDimensions} for this popup. Called just once.
* You can use fixed dimensions or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and
* {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}.
*
* @return a PopupDimensions object
*/
// Called at first to understand which dimensions to use for the popup.
@NonNull
protected PopupDimensions getPopupDimensions() {
return new PopupDimensions();
}
/**
* Perform firther initialization here. Called after {@link #getView()},
* each time the popup is shown.
*/
protected abstract void onViewShown();
/**
* Called to update the view to filter results with the query.
* It is called any time the popup is shown, and any time the text changes and query is updated.
*
* @param query query from the edit text, to filter our results
*/
protected abstract void onQuery(@Nullable CharSequence query);
/**
* Called when the popup is hidden, to release resources.
*/
protected abstract void onViewHidden();
/**
* @return this presenter context
*/
@NonNull
protected final Context getContext() {
return context;
}
/**
* @return whether we are showing currently
*/
@SuppressWarnings("unused")
protected final boolean isShowing() {
return isShowing;
}
final void showView() {
isShowing = true;
onViewShown();
}
final void hideView() {
isShowing = false;
onViewHidden();
}
public interface ClickProvider<T> {
void click(@NonNull T item);
}
/**
* Provides width, height, maxWidth and maxHeight for the popup.
* @see #getPopupDimensions()
*/
@SuppressWarnings("WeakerAccess")
public static class PopupDimensions {
public int width = ViewGroup.LayoutParams.WRAP_CONTENT;
public int height = ViewGroup.LayoutParams.WRAP_CONTENT;
public int maxWidth = Integer.MAX_VALUE;
public int maxHeight = Integer.MAX_VALUE;
}
}

View File

@ -0,0 +1,184 @@
package com.otaliastudios.autocomplete;
import android.text.Spannable;
import android.text.Spanned;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A special {@link AutocompletePolicy} for cases when you want to trigger the popup when a
* certain character is shown.
*
* For instance, this might be the case for hashtags ('#') or usernames ('@') or whatever you wish.
* Passing this to {@link Autocomplete.Builder} ensures the following behavior (assuming '@'):
* - text "@john" : presenter will be passed the query "john"
* - text "You should see this @j" : presenter will be passed the query "j"
* - text "You should see this @john @m" : presenter will be passed the query "m"
*/
public class CharPolicy implements AutocompletePolicy {
private final static String TAG = CharPolicy.class.getSimpleName();
private final static boolean DEBUG = false;
private static void log(@NonNull String log) {
if (DEBUG) Log.e(TAG, log);
}
private final char CH;
private final int[] INT = new int[2];
private boolean needSpaceBefore = true;
/**
* Constructs a char policy for the given character.
*
* @param trigger the triggering character.
*/
public CharPolicy(char trigger) {
CH = trigger;
}
/**
* Constructs a char policy for the given character.
* You can choose whether a whitespace is needed before 'trigger'.
*
* @param trigger the triggering character.
* @param needSpaceBefore whether we need a space before trigger
*/
@SuppressWarnings("unused")
public CharPolicy(char trigger, boolean needSpaceBefore) {
CH = trigger;
this.needSpaceBefore = needSpaceBefore;
}
/**
* Can be overriden to understand which characters are valid. The default implementation
* returns true for any character except whitespaces.
*
* @param ch the character
* @return whether it's valid part of a query
*/
@SuppressWarnings("WeakerAccess")
protected boolean isValidChar(char ch) {
return !Character.isWhitespace(ch);
}
@Nullable
private int[] checkText(@NonNull Spannable text, int cursorPos) {
final int spanEnd = cursorPos;
char last = 'x';
cursorPos -= 1; // If the cursor is at the end, we will have cursorPos = length. Go back by 1.
while (cursorPos >= 0 && last != CH) {
char ch = text.charAt(cursorPos);
log("checkText: char is "+ch);
if (isValidChar(ch)) {
// We are going back
log("checkText: char is valid");
cursorPos -= 1;
last = ch;
} else {
// We got a whitespace before getting a CH. This is invalid.
log("checkText: char is not valid, returning NULL");
return null;
}
}
cursorPos += 1; // + 1 because we end BEHIND the valid selection
// Start checking.
if (cursorPos == 0 && last != CH) {
// We got to the start of the string, and no CH was encountered. Nothing to do.
log("checkText: got to start but no CH, returning NULL");
return null;
}
// Additional checks for cursorPos - 1
if (cursorPos > 0 && needSpaceBefore) {
char ch = text.charAt(cursorPos-1);
if (!Character.isWhitespace(ch)) {
log("checkText: char before is not whitespace, returning NULL");
return null;
}
}
// All seems OK.
final int spanStart = cursorPos + 1; // + 1 because we want to exclude CH from the query
INT[0] = spanStart;
INT[1] = spanEnd;
log("checkText: found! cursorPos="+cursorPos);
log("checkText: found! spanStart="+spanStart);
log("checkText: found! spanEnd="+spanEnd);
return INT;
}
@Override
public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) {
// Returning true if, right before cursorPos, we have a word starting with @.
log("shouldShowPopup: text is "+text);
log("shouldShowPopup: cursorPos is "+cursorPos);
int[] show = checkText(text, cursorPos);
if (show != null) {
text.setSpan(new QuerySpan(), show[0], show[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE);
return true;
}
log("shouldShowPopup: returning false");
return false;
}
@Override
public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) {
log("shouldDismissPopup: text is "+text);
log("shouldDismissPopup: cursorPos is "+cursorPos);
boolean dismiss = checkText(text, cursorPos) == null;
log("shouldDismissPopup: returning "+dismiss);
return dismiss;
}
@NonNull
@Override
public CharSequence getQuery(@NonNull Spannable text) {
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
if (span == null || span.length == 0) {
// Should never happen.
log("getQuery: there's no span!");
return "";
}
log("getQuery: found spans: "+span.length);
QuerySpan sp = span[0];
log("getQuery: span start is "+text.getSpanStart(sp));
log("getQuery: span end is "+text.getSpanEnd(sp));
CharSequence seq = text.subSequence(text.getSpanStart(sp), text.getSpanEnd(sp));
log("getQuery: returning "+seq);
return seq;
}
@Override
public void onDismiss(@NonNull Spannable text) {
// Remove any span added by shouldShow. Should be useless, but anyway.
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
for (QuerySpan s : span) {
text.removeSpan(s);
}
}
private static class QuerySpan {}
/**
* Returns the current query out of the given Spannable.
* @param text the anchor text
* @return an int[] with query start and query end positions
*/
@Nullable
public static int[] getQueryRange(@NonNull Spannable text) {
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
if (span == null || span.length == 0) return null;
if (span.length > 1) {
// Won't happen
log("getQueryRange: ERR: MORE THAN ONE QuerySpan.");
}
QuerySpan sp = span[0];
return new int[]{text.getSpanStart(sp), text.getSpanEnd(sp)};
}
}

View File

@ -0,0 +1,152 @@
package com.otaliastudios.autocomplete;
import android.content.Context;
import android.database.DataSetObserver;
import android.view.ViewGroup;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* Simple {@link AutocompletePresenter} implementation that hosts a {@link RecyclerView}.
* Supports {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} natively.
* The only contract is to
*
* - provide a {@link RecyclerView.Adapter} in {@link #instantiateAdapter()}
* - call {@link #dispatchClick(Object)} when an object is clicked
* - update your data during {@link #onQuery(CharSequence)}
*
* @param <T> your model object (the object displayed by the list)
*/
public abstract class RecyclerViewPresenter<T> extends AutocompletePresenter<T> {
private RecyclerView recycler;
private ClickProvider<T> clicks;
private Observer observer;
public RecyclerViewPresenter(@NonNull Context context) {
super(context);
}
@Override
protected final void registerClickProvider(@NonNull ClickProvider<T> provider) {
this.clicks = provider;
}
@Override
protected final void registerDataSetObserver(@NonNull DataSetObserver observer) {
this.observer = new Observer(observer);
}
@NonNull
@Override
protected ViewGroup getView() {
recycler = new RecyclerView(getContext());
RecyclerView.Adapter adapter = instantiateAdapter();
recycler.setAdapter(adapter);
recycler.setLayoutManager(instantiateLayoutManager());
if (observer != null) {
adapter.registerAdapterDataObserver(observer);
observer = null;
}
return recycler;
}
@Override
protected void onViewShown() {}
@CallSuper
@Override
protected void onViewHidden() {
recycler = null;
observer = null;
}
@SuppressWarnings("unused")
@Nullable
protected final RecyclerView getRecyclerView() {
return recycler;
}
/**
* Dispatch click event to {@link AutocompleteCallback}.
* Should be called when items are clicked.
*
* @param item the clicked item.
*/
protected final void dispatchClick(@NonNull T item) {
if (clicks != null) clicks.click(item);
}
/**
* Request that the popup should recompute its dimensions based on a recent change in
* the view being displayed.
*
* This is already managed internally for {@link RecyclerView} events.
* Only use it for changes in other views that you have added to the popup,
* and only if one of the dimensions for the popup is WRAP_CONTENT .
*/
@SuppressWarnings("unused")
protected final void dispatchLayoutChange() {
if (observer != null) observer.onChanged();
}
/**
* Provide an adapter for the recycler.
* This should be a fresh instance every time this is called.
*
* @return a new adapter.
*/
@NonNull
protected abstract RecyclerView.Adapter instantiateAdapter();
/**
* Provides a layout manager for the recycler.
* This should be a fresh instance every time this is called.
* Defaults to a vertical LinearLayoutManager, which is guaranteed to work well.
*
* @return a new layout manager.
*/
@SuppressWarnings("WeakerAccess")
@NonNull
protected RecyclerView.LayoutManager instantiateLayoutManager() {
return new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
}
private final static class Observer extends RecyclerView.AdapterDataObserver {
private DataSetObserver root;
Observer(@NonNull DataSetObserver root) {
this.root = root;
}
@Override
public void onChanged() {
root.onChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
root.onChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
root.onChanged();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
root.onChanged();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
root.onChanged();
}
}
}

View File

@ -0,0 +1,26 @@
apply plugin: 'com.android.library'
android {
namespace "me.dm7.barcodescanner.core"
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
}
compileOptions {
sourceCompatibility versions.sourceCompat
targetCompatibility versions.targetCompat
}
}
dependencies {
implementation 'androidx.annotation:annotation-jvm:1.6.0'
}
afterEvaluate {
tasks.findAll { it.name.startsWith("lint") }.each {
it.enabled = false
}
}

View File

@ -0,0 +1,339 @@
package me.dm7.barcodescanner.core;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import androidx.annotation.ColorInt;
public abstract class BarcodeScannerView extends FrameLayout implements Camera.PreviewCallback {
private CameraWrapper mCameraWrapper;
private CameraPreview mPreview;
private IViewFinder mViewFinderView;
private Rect mFramingRectInPreview;
private CameraHandlerThread mCameraHandlerThread;
private Boolean mFlashState;
private boolean mAutofocusState = true;
private boolean mShouldScaleToFill = true;
private boolean mIsLaserEnabled = true;
@ColorInt private int mLaserColor = getResources().getColor(R.color.viewfinder_laser);
@ColorInt private int mBorderColor = getResources().getColor(R.color.viewfinder_border);
private int mMaskColor = getResources().getColor(R.color.viewfinder_mask);
private int mBorderWidth = getResources().getInteger(R.integer.viewfinder_border_width);
private int mBorderLength = getResources().getInteger(R.integer.viewfinder_border_length);
private boolean mRoundedCorner = false;
private int mCornerRadius = 0;
private boolean mSquaredFinder = false;
private float mBorderAlpha = 1.0f;
private int mViewFinderOffset = 0;
private float mAspectTolerance = 0.1f;
public BarcodeScannerView(Context context) {
super(context);
init();
}
public BarcodeScannerView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
TypedArray a = context.getTheme().obtainStyledAttributes(
attributeSet,
R.styleable.BarcodeScannerView,
0, 0);
try {
setShouldScaleToFill(a.getBoolean(R.styleable.BarcodeScannerView_shouldScaleToFill, true));
mIsLaserEnabled = a.getBoolean(R.styleable.BarcodeScannerView_laserEnabled, mIsLaserEnabled);
mLaserColor = a.getColor(R.styleable.BarcodeScannerView_laserColor, mLaserColor);
mBorderColor = a.getColor(R.styleable.BarcodeScannerView_borderColor, mBorderColor);
mMaskColor = a.getColor(R.styleable.BarcodeScannerView_maskColor, mMaskColor);
mBorderWidth = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_borderWidth, mBorderWidth);
mBorderLength = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_borderLength, mBorderLength);
mRoundedCorner = a.getBoolean(R.styleable.BarcodeScannerView_roundedCorner, mRoundedCorner);
mCornerRadius = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_cornerRadius, mCornerRadius);
mSquaredFinder = a.getBoolean(R.styleable.BarcodeScannerView_squaredFinder, mSquaredFinder);
mBorderAlpha = a.getFloat(R.styleable.BarcodeScannerView_borderAlpha, mBorderAlpha);
mViewFinderOffset = a.getDimensionPixelSize(R.styleable.BarcodeScannerView_finderOffset, mViewFinderOffset);
} finally {
a.recycle();
}
init();
}
private void init() {
mViewFinderView = createViewFinderView(getContext());
}
public final void setupLayout(CameraWrapper cameraWrapper) {
removeAllViews();
mPreview = new CameraPreview(getContext(), cameraWrapper, this);
mPreview.setAspectTolerance(mAspectTolerance);
mPreview.setShouldScaleToFill(mShouldScaleToFill);
if (!mShouldScaleToFill) {
RelativeLayout relativeLayout = new RelativeLayout(getContext());
relativeLayout.setGravity(Gravity.CENTER);
relativeLayout.setBackgroundColor(Color.BLACK);
relativeLayout.addView(mPreview);
addView(relativeLayout);
} else {
addView(mPreview);
}
if (mViewFinderView instanceof View) {
addView((View) mViewFinderView);
} else {
throw new IllegalArgumentException("IViewFinder object returned by " +
"'createViewFinderView()' should be instance of android.view.View");
}
}
/**
* <p>Method that creates view that represents visual appearance of a barcode scanner</p>
* <p>Override it to provide your own view for visual appearance of a barcode scanner</p>
*
* @param context {@link Context}
* @return {@link android.view.View} that implements {@link ViewFinderView}
*/
protected IViewFinder createViewFinderView(Context context) {
ViewFinderView viewFinderView = new ViewFinderView(context);
viewFinderView.setBorderColor(mBorderColor);
viewFinderView.setLaserColor(mLaserColor);
viewFinderView.setLaserEnabled(mIsLaserEnabled);
viewFinderView.setBorderStrokeWidth(mBorderWidth);
viewFinderView.setBorderLineLength(mBorderLength);
viewFinderView.setMaskColor(mMaskColor);
viewFinderView.setBorderCornerRounded(mRoundedCorner);
viewFinderView.setBorderCornerRadius(mCornerRadius);
viewFinderView.setSquareViewFinder(mSquaredFinder);
viewFinderView.setViewFinderOffset(mViewFinderOffset);
return viewFinderView;
}
public void setLaserColor(int laserColor) {
mLaserColor = laserColor;
mViewFinderView.setLaserColor(mLaserColor);
mViewFinderView.setupViewFinder();
}
public void setMaskColor(int maskColor) {
mMaskColor = maskColor;
mViewFinderView.setMaskColor(mMaskColor);
mViewFinderView.setupViewFinder();
}
public void setBorderColor(int borderColor) {
mBorderColor = borderColor;
mViewFinderView.setBorderColor(mBorderColor);
mViewFinderView.setupViewFinder();
}
public void setBorderStrokeWidth(int borderStrokeWidth) {
mBorderWidth = borderStrokeWidth;
mViewFinderView.setBorderStrokeWidth(mBorderWidth);
mViewFinderView.setupViewFinder();
}
public void setBorderLineLength(int borderLineLength) {
mBorderLength = borderLineLength;
mViewFinderView.setBorderLineLength(mBorderLength);
mViewFinderView.setupViewFinder();
}
public void setLaserEnabled(boolean isLaserEnabled) {
mIsLaserEnabled = isLaserEnabled;
mViewFinderView.setLaserEnabled(mIsLaserEnabled);
mViewFinderView.setupViewFinder();
}
public void setIsBorderCornerRounded(boolean isBorderCornerRounded) {
mRoundedCorner = isBorderCornerRounded;
mViewFinderView.setBorderCornerRounded(mRoundedCorner);
mViewFinderView.setupViewFinder();
}
public void setBorderCornerRadius(int borderCornerRadius) {
mCornerRadius = borderCornerRadius;
mViewFinderView.setBorderCornerRadius(mCornerRadius);
mViewFinderView.setupViewFinder();
}
public void setSquareViewFinder(boolean isSquareViewFinder) {
mSquaredFinder = isSquareViewFinder;
mViewFinderView.setSquareViewFinder(mSquaredFinder);
mViewFinderView.setupViewFinder();
}
public void setBorderAlpha(float borderAlpha) {
mBorderAlpha = borderAlpha;
mViewFinderView.setBorderAlpha(mBorderAlpha);
mViewFinderView.setupViewFinder();
}
public void startCamera(int cameraId) {
if(mCameraHandlerThread == null) {
mCameraHandlerThread = new CameraHandlerThread(this);
}
mCameraHandlerThread.startCamera(cameraId);
}
public void setupCameraPreview(CameraWrapper cameraWrapper) {
mCameraWrapper = cameraWrapper;
if(mCameraWrapper != null) {
setupLayout(mCameraWrapper);
mViewFinderView.setupViewFinder();
if(mFlashState != null) {
setFlash(mFlashState);
}
setAutoFocus(mAutofocusState);
}
}
public void startCamera() {
startCamera(CameraUtils.getDefaultCameraId());
}
public void stopCamera() {
if(mCameraWrapper != null) {
mPreview.stopCameraPreview();
mPreview.setCamera(null, null);
mCameraWrapper.mCamera.release();
mCameraWrapper = null;
}
if(mCameraHandlerThread != null) {
mCameraHandlerThread.quit();
mCameraHandlerThread = null;
}
}
public void stopCameraPreview() {
if(mPreview != null) {
mPreview.stopCameraPreview();
}
}
protected void resumeCameraPreview() {
if(mPreview != null) {
mPreview.showCameraPreview();
}
}
public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
if (mFramingRectInPreview == null) {
Rect framingRect = mViewFinderView.getFramingRect();
int viewFinderViewWidth = mViewFinderView.getWidth();
int viewFinderViewHeight = mViewFinderView.getHeight();
if (framingRect == null || viewFinderViewWidth == 0 || viewFinderViewHeight == 0) {
return null;
}
Rect rect = new Rect(framingRect);
if(previewWidth < viewFinderViewWidth) {
rect.left = rect.left * previewWidth / viewFinderViewWidth;
rect.right = rect.right * previewWidth / viewFinderViewWidth;
}
if(previewHeight < viewFinderViewHeight) {
rect.top = rect.top * previewHeight / viewFinderViewHeight;
rect.bottom = rect.bottom * previewHeight / viewFinderViewHeight;
}
mFramingRectInPreview = rect;
}
return mFramingRectInPreview;
}
public void setFlash(boolean flag) {
mFlashState = flag;
if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) {
Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters();
if(flag) {
if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) {
return;
}
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
} else {
if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) {
return;
}
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
}
mCameraWrapper.mCamera.setParameters(parameters);
}
}
public boolean getFlash() {
if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) {
Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters();
if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) {
return true;
} else {
return false;
}
}
return false;
}
public void toggleFlash() {
if(mCameraWrapper != null && CameraUtils.isFlashSupported(mCameraWrapper.mCamera)) {
Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters();
if(parameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_TORCH)) {
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
} else {
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
}
mCameraWrapper.mCamera.setParameters(parameters);
}
}
public void setAutoFocus(boolean state) {
mAutofocusState = state;
if(mPreview != null) {
mPreview.setAutoFocus(state);
}
}
public void setShouldScaleToFill(boolean shouldScaleToFill) {
mShouldScaleToFill = shouldScaleToFill;
}
public void setAspectTolerance(float aspectTolerance) {
mAspectTolerance = aspectTolerance;
}
public byte[] getRotatedData(byte[] data, Camera camera) {
Camera.Parameters parameters = camera.getParameters();
Camera.Size size = parameters.getPreviewSize();
int width = size.width;
int height = size.height;
int rotationCount = getRotationCount();
if(rotationCount == 1 || rotationCount == 3) {
for (int i = 0; i < rotationCount; i++) {
byte[] rotatedData = new byte[data.length];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++)
rotatedData[x * height + height - y - 1] = data[x + y * width];
}
data = rotatedData;
int tmp = width;
width = height;
height = tmp;
}
}
return data;
}
public int getRotationCount() {
int displayOrientation = mPreview.getDisplayOrientation();
return displayOrientation / 90;
}
}

View File

@ -0,0 +1,37 @@
package me.dm7.barcodescanner.core;
import android.hardware.Camera;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
// This code is mostly based on the top answer here: http://stackoverflow.com/questions/18149964/best-use-of-handlerthread-over-other-similar-classes
public class CameraHandlerThread extends HandlerThread {
private static final String LOG_TAG = "CameraHandlerThread";
private BarcodeScannerView mScannerView;
public CameraHandlerThread(BarcodeScannerView scannerView) {
super("CameraHandlerThread");
mScannerView = scannerView;
start();
}
public void startCamera(final int cameraId) {
Handler localHandler = new Handler(getLooper());
localHandler.post(new Runnable() {
@Override
public void run() {
final Camera camera = CameraUtils.getCameraInstance(cameraId);
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(new Runnable() {
@Override
public void run() {
mScannerView.setupCameraPreview(CameraWrapper.getWrapper(camera, cameraId));
}
});
}
});
}
}

View File

@ -0,0 +1,312 @@
package me.dm7.barcodescanner.core;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.Camera;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import java.util.List;
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "CameraPreview";
private CameraWrapper mCameraWrapper;
private Handler mAutoFocusHandler;
private boolean mPreviewing = true;
private boolean mAutoFocus = true;
private boolean mSurfaceCreated = false;
private boolean mShouldScaleToFill = true;
private Camera.PreviewCallback mPreviewCallback;
private float mAspectTolerance = 0.1f;
public CameraPreview(Context context, CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) {
super(context);
init(cameraWrapper, previewCallback);
}
public CameraPreview(Context context, AttributeSet attrs, CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) {
super(context, attrs);
init(cameraWrapper, previewCallback);
}
public void init(CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) {
setCamera(cameraWrapper, previewCallback);
mAutoFocusHandler = new Handler();
getHolder().addCallback(this);
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void setCamera(CameraWrapper cameraWrapper, Camera.PreviewCallback previewCallback) {
mCameraWrapper = cameraWrapper;
mPreviewCallback = previewCallback;
}
public void setShouldScaleToFill(boolean scaleToFill) {
mShouldScaleToFill = scaleToFill;
}
public void setAspectTolerance(float aspectTolerance) {
mAspectTolerance = aspectTolerance;
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
mSurfaceCreated = true;
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i2, int i3) {
if(surfaceHolder.getSurface() == null) {
return;
}
stopCameraPreview();
showCameraPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mSurfaceCreated = false;
stopCameraPreview();
}
public void showCameraPreview() {
if(mCameraWrapper != null) {
try {
getHolder().addCallback(this);
mPreviewing = true;
setupCameraParameters();
mCameraWrapper.mCamera.setPreviewDisplay(getHolder());
mCameraWrapper.mCamera.setDisplayOrientation(getDisplayOrientation());
mCameraWrapper.mCamera.setOneShotPreviewCallback(mPreviewCallback);
mCameraWrapper.mCamera.startPreview();
if(mAutoFocus) {
if (mSurfaceCreated) { // check if surface created before using autofocus
safeAutoFocus();
} else {
scheduleAutoFocus(); // wait 1 sec and then do check again
}
}
} catch (Exception e) {
Log.e(TAG, e.toString(), e);
}
}
}
public void safeAutoFocus() {
try {
mCameraWrapper.mCamera.autoFocus(autoFocusCB);
} catch (RuntimeException re) {
// Horrible hack to deal with autofocus errors on Sony devices
// See https://github.com/dm77/barcodescanner/issues/7 for example
scheduleAutoFocus(); // wait 1 sec and then do check again
}
}
public void stopCameraPreview() {
if(mCameraWrapper != null) {
try {
mPreviewing = false;
getHolder().removeCallback(this);
mCameraWrapper.mCamera.cancelAutoFocus();
mCameraWrapper.mCamera.setOneShotPreviewCallback(null);
mCameraWrapper.mCamera.stopPreview();
} catch(Exception e) {
Log.e(TAG, e.toString(), e);
}
}
}
public void setupCameraParameters() {
Camera.Size optimalSize = getOptimalPreviewSize();
Camera.Parameters parameters = mCameraWrapper.mCamera.getParameters();
parameters.setPreviewSize(optimalSize.width, optimalSize.height);
mCameraWrapper.mCamera.setParameters(parameters);
adjustViewSize(optimalSize);
}
private void adjustViewSize(Camera.Size cameraSize) {
Point previewSize = convertSizeToLandscapeOrientation(new Point(getWidth(), getHeight()));
float cameraRatio = ((float) cameraSize.width) / cameraSize.height;
float screenRatio = ((float) previewSize.x) / previewSize.y;
if (screenRatio > cameraRatio) {
setViewSize((int) (previewSize.y * cameraRatio), previewSize.y);
} else {
setViewSize(previewSize.x, (int) (previewSize.x / cameraRatio));
}
}
@SuppressWarnings("SuspiciousNameCombination")
private Point convertSizeToLandscapeOrientation(Point size) {
if (getDisplayOrientation() % 180 == 0) {
return size;
} else {
return new Point(size.y, size.x);
}
}
@SuppressWarnings("SuspiciousNameCombination")
private void setViewSize(int width, int height) {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
int tmpWidth;
int tmpHeight;
if (getDisplayOrientation() % 180 == 0) {
tmpWidth = width;
tmpHeight = height;
} else {
tmpWidth = height;
tmpHeight = width;
}
if (mShouldScaleToFill) {
int parentWidth = ((View) getParent()).getWidth();
int parentHeight = ((View) getParent()).getHeight();
float ratioWidth = (float) parentWidth / (float) tmpWidth;
float ratioHeight = (float) parentHeight / (float) tmpHeight;
float compensation;
if (ratioWidth > ratioHeight) {
compensation = ratioWidth;
} else {
compensation = ratioHeight;
}
tmpWidth = Math.round(tmpWidth * compensation);
tmpHeight = Math.round(tmpHeight * compensation);
}
layoutParams.width = tmpWidth;
layoutParams.height = tmpHeight;
setLayoutParams(layoutParams);
}
public int getDisplayOrientation() {
if (mCameraWrapper == null) {
//If we don't have a camera set there is no orientation so return dummy value
return 0;
}
Camera.CameraInfo info = new Camera.CameraInfo();
if(mCameraWrapper.mCameraId == -1) {
Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, info);
} else {
Camera.getCameraInfo(mCameraWrapper.mCameraId, info);
}
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int rotation = display.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
return result;
}
private Camera.Size getOptimalPreviewSize() {
if(mCameraWrapper == null) {
return null;
}
List<Camera.Size> sizes = mCameraWrapper.mCamera.getParameters().getSupportedPreviewSizes();
int w = getWidth();
int h = getHeight();
if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) {
int portraitWidth = h;
h = w;
w = portraitWidth;
}
double targetRatio = (double) w / h;
if (sizes == null) return null;
Camera.Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
int targetHeight = h;
// Try to find an size match aspect ratio and size
for (Camera.Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > mAspectTolerance) continue;
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
public void setAutoFocus(boolean state) {
if(mCameraWrapper != null && mPreviewing) {
if(state == mAutoFocus) {
return;
}
mAutoFocus = state;
if(mAutoFocus) {
if (mSurfaceCreated) { // check if surface created before using autofocus
Log.v(TAG, "Starting autofocus");
safeAutoFocus();
} else {
scheduleAutoFocus(); // wait 1 sec and then do check again
}
} else {
Log.v(TAG, "Cancelling autofocus");
mCameraWrapper.mCamera.cancelAutoFocus();
}
}
}
private Runnable doAutoFocus = new Runnable() {
public void run() {
if(mCameraWrapper != null && mPreviewing && mAutoFocus && mSurfaceCreated) {
safeAutoFocus();
}
}
};
// Mimic continuous auto-focusing
Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() {
public void onAutoFocus(boolean success, Camera camera) {
scheduleAutoFocus();
}
};
private void scheduleAutoFocus() {
mAutoFocusHandler.postDelayed(doAutoFocus, 1000);
}
}

View File

@ -0,0 +1,63 @@
package me.dm7.barcodescanner.core;
import android.hardware.Camera;
import java.util.List;
public class CameraUtils {
/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance() {
return getCameraInstance(getDefaultCameraId());
}
/** Favor back-facing camera by default. If none exists, fallback to whatever camera is available **/
public static int getDefaultCameraId() {
int numberOfCameras = Camera.getNumberOfCameras();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
int defaultCameraId = -1;
for (int i = 0; i < numberOfCameras; i++) {
defaultCameraId = i;
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
return i;
}
}
return defaultCameraId;
}
/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance(int cameraId) {
Camera c = null;
try {
if(cameraId == -1) {
c = Camera.open(); // attempt to get a Camera instance
} else {
c = Camera.open(cameraId); // attempt to get a Camera instance
}
}
catch (Exception e) {
// Camera is not available (in use or does not exist)
}
return c; // returns null if camera is unavailable
}
public static boolean isFlashSupported(Camera camera) {
/* Credits: Top answer at http://stackoverflow.com/a/19599365/868173 */
if (camera != null) {
Camera.Parameters parameters = camera.getParameters();
if (parameters.getFlashMode() == null) {
return false;
}
List<String> supportedFlashModes = parameters.getSupportedFlashModes();
if (supportedFlashModes == null || supportedFlashModes.isEmpty() || supportedFlashModes.size() == 1 && supportedFlashModes.get(0).equals(Camera.Parameters.FLASH_MODE_OFF)) {
return false;
}
} else {
return false;
}
return true;
}
}

View File

@ -0,0 +1,23 @@
package me.dm7.barcodescanner.core;
import android.hardware.Camera;
import androidx.annotation.NonNull;
public class CameraWrapper {
public final Camera mCamera;
public final int mCameraId;
private CameraWrapper(@NonNull Camera camera, int cameraId) {
this.mCamera = camera;
this.mCameraId = cameraId;
}
public static CameraWrapper getWrapper(Camera camera, int cameraId) {
if (camera == null) {
return null;
} else {
return new CameraWrapper(camera, cameraId);
}
}
}

View File

@ -0,0 +1,41 @@
package me.dm7.barcodescanner.core;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
public class DisplayUtils {
public static Point getScreenResolution(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point screenResolution = new Point();
if (android.os.Build.VERSION.SDK_INT >= 13) {
display.getSize(screenResolution);
} else {
screenResolution.set(display.getWidth(), display.getHeight());
}
return screenResolution;
}
public static int getScreenOrientation(Context context)
{
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
int orientation = Configuration.ORIENTATION_UNDEFINED;
if(display.getWidth()==display.getHeight()){
orientation = Configuration.ORIENTATION_SQUARE;
} else{
if(display.getWidth() < display.getHeight()){
orientation = Configuration.ORIENTATION_PORTRAIT;
}else {
orientation = Configuration.ORIENTATION_LANDSCAPE;
}
}
return orientation;
}
}

View File

@ -0,0 +1,53 @@
package me.dm7.barcodescanner.core;
import android.graphics.Rect;
public interface IViewFinder {
void setLaserColor(int laserColor);
void setMaskColor(int maskColor);
void setBorderColor(int borderColor);
void setBorderStrokeWidth(int borderStrokeWidth);
void setBorderLineLength(int borderLineLength);
void setLaserEnabled(boolean isLaserEnabled);
void setBorderCornerRounded(boolean isBorderCornersRounded);
void setBorderAlpha(float alpha);
void setBorderCornerRadius(int borderCornersRadius);
void setViewFinderOffset(int offset);
void setSquareViewFinder(boolean isSquareViewFinder);
/**
* Method that executes when Camera preview is starting.
* It is recommended to update framing rect here and invalidate view after that. <br/>
* For example see: {@link ViewFinderView#setupViewFinder()}
*/
void setupViewFinder();
/**
* Provides {@link Rect} that identifies area where barcode scanner can detect visual codes
* <p>Note: This rect is a area representation in absolute pixel values. <br/>
* For example: <br/>
* If View's size is 1024x800 so framing rect might be 500x400</p>
*
* @return {@link Rect} that identifies barcode scanner area
*/
Rect getFramingRect();
/**
* Width of a {@link android.view.View} that implements this interface
* <p>Note: this is already implemented in {@link android.view.View},
* so you don't need to override method and provide your implementation</p>
*
* @return width of a view
*/
int getWidth();
/**
* Height of a {@link android.view.View} that implements this interface
* <p>Note: this is already implemented in {@link android.view.View},
* so you don't need to override method and provide your implementation</p>
*
* @return height of a view
*/
int getHeight();
}

View File

@ -0,0 +1,259 @@
package me.dm7.barcodescanner.core;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
public class ViewFinderView extends View implements IViewFinder {
private static final String TAG = "ViewFinderView";
private Rect mFramingRect;
private static final float PORTRAIT_WIDTH_RATIO = 6f/8;
private static final float PORTRAIT_WIDTH_HEIGHT_RATIO = 0.75f;
private static final float LANDSCAPE_HEIGHT_RATIO = 5f/8;
private static final float LANDSCAPE_WIDTH_HEIGHT_RATIO = 1.4f;
private static final int MIN_DIMENSION_DIFF = 50;
private static final float DEFAULT_SQUARE_DIMENSION_RATIO = 5f / 8;
private static final int[] SCANNER_ALPHA = {0, 64, 128, 192, 255, 192, 128, 64};
private int scannerAlpha;
private static final int POINT_SIZE = 10;
private static final long ANIMATION_DELAY = 80l;
private final int mDefaultLaserColor = getResources().getColor(R.color.viewfinder_laser);
private final int mDefaultMaskColor = getResources().getColor(R.color.viewfinder_mask);
private final int mDefaultBorderColor = getResources().getColor(R.color.viewfinder_border);
private final int mDefaultBorderStrokeWidth = getResources().getInteger(R.integer.viewfinder_border_width);
private final int mDefaultBorderLineLength = getResources().getInteger(R.integer.viewfinder_border_length);
protected Paint mLaserPaint;
protected Paint mFinderMaskPaint;
protected Paint mBorderPaint;
protected int mBorderLineLength;
protected boolean mSquareViewFinder;
private boolean mIsLaserEnabled;
private float mBordersAlpha;
private int mViewFinderOffset = 0;
public ViewFinderView(Context context) {
super(context);
init();
}
public ViewFinderView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
init();
}
private void init() {
//set up laser paint
mLaserPaint = new Paint();
mLaserPaint.setColor(mDefaultLaserColor);
mLaserPaint.setStyle(Paint.Style.FILL);
//finder mask paint
mFinderMaskPaint = new Paint();
mFinderMaskPaint.setColor(mDefaultMaskColor);
//border paint
mBorderPaint = new Paint();
mBorderPaint.setColor(mDefaultBorderColor);
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setStrokeWidth(mDefaultBorderStrokeWidth);
mBorderPaint.setAntiAlias(true);
mBorderLineLength = mDefaultBorderLineLength;
}
@Override
public void setLaserColor(int laserColor) {
mLaserPaint.setColor(laserColor);
}
@Override
public void setMaskColor(int maskColor) {
mFinderMaskPaint.setColor(maskColor);
}
@Override
public void setBorderColor(int borderColor) {
mBorderPaint.setColor(borderColor);
}
@Override
public void setBorderStrokeWidth(int borderStrokeWidth) {
mBorderPaint.setStrokeWidth(borderStrokeWidth);
}
@Override
public void setBorderLineLength(int borderLineLength) {
mBorderLineLength = borderLineLength;
}
@Override
public void setLaserEnabled(boolean isLaserEnabled) { mIsLaserEnabled = isLaserEnabled; }
@Override
public void setBorderCornerRounded(boolean isBorderCornersRounded) {
if (isBorderCornersRounded) {
mBorderPaint.setStrokeJoin(Paint.Join.ROUND);
} else {
mBorderPaint.setStrokeJoin(Paint.Join.BEVEL);
}
}
@Override
public void setBorderAlpha(float alpha) {
int colorAlpha = (int) (255 * alpha);
mBordersAlpha = alpha;
mBorderPaint.setAlpha(colorAlpha);
}
@Override
public void setBorderCornerRadius(int borderCornersRadius) {
mBorderPaint.setPathEffect(new CornerPathEffect(borderCornersRadius));
}
@Override
public void setViewFinderOffset(int offset) {
mViewFinderOffset = offset;
}
// TODO: Need a better way to configure this. Revisit when working on 2.0
@Override
public void setSquareViewFinder(boolean set) {
mSquareViewFinder = set;
}
public void setupViewFinder() {
updateFramingRect();
invalidate();
}
public Rect getFramingRect() {
return mFramingRect;
}
@Override
public void onDraw(Canvas canvas) {
if(getFramingRect() == null) {
return;
}
drawViewFinderMask(canvas);
drawViewFinderBorder(canvas);
if (mIsLaserEnabled) {
drawLaser(canvas);
}
}
public void drawViewFinderMask(Canvas canvas) {
int width = canvas.getWidth();
int height = canvas.getHeight();
Rect framingRect = getFramingRect();
canvas.drawRect(0, 0, width, framingRect.top, mFinderMaskPaint);
canvas.drawRect(0, framingRect.top, framingRect.left, framingRect.bottom + 1, mFinderMaskPaint);
canvas.drawRect(framingRect.right + 1, framingRect.top, width, framingRect.bottom + 1, mFinderMaskPaint);
canvas.drawRect(0, framingRect.bottom + 1, width, height, mFinderMaskPaint);
}
public void drawViewFinderBorder(Canvas canvas) {
Rect framingRect = getFramingRect();
// Top-left corner
Path path = new Path();
path.moveTo(framingRect.left, framingRect.top + mBorderLineLength);
path.lineTo(framingRect.left, framingRect.top);
path.lineTo(framingRect.left + mBorderLineLength, framingRect.top);
canvas.drawPath(path, mBorderPaint);
// Top-right corner
path.moveTo(framingRect.right, framingRect.top + mBorderLineLength);
path.lineTo(framingRect.right, framingRect.top);
path.lineTo(framingRect.right - mBorderLineLength, framingRect.top);
canvas.drawPath(path, mBorderPaint);
// Bottom-right corner
path.moveTo(framingRect.right, framingRect.bottom - mBorderLineLength);
path.lineTo(framingRect.right, framingRect.bottom);
path.lineTo(framingRect.right - mBorderLineLength, framingRect.bottom);
canvas.drawPath(path, mBorderPaint);
// Bottom-left corner
path.moveTo(framingRect.left, framingRect.bottom - mBorderLineLength);
path.lineTo(framingRect.left, framingRect.bottom);
path.lineTo(framingRect.left + mBorderLineLength, framingRect.bottom);
canvas.drawPath(path, mBorderPaint);
}
public void drawLaser(Canvas canvas) {
Rect framingRect = getFramingRect();
// Draw a red "laser scanner" line through the middle to show decoding is active
mLaserPaint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
int middle = framingRect.height() / 2 + framingRect.top;
canvas.drawRect(framingRect.left + 2, middle - 1, framingRect.right - 1, middle + 2, mLaserPaint);
postInvalidateDelayed(ANIMATION_DELAY,
framingRect.left - POINT_SIZE,
framingRect.top - POINT_SIZE,
framingRect.right + POINT_SIZE,
framingRect.bottom + POINT_SIZE);
}
@Override
protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) {
updateFramingRect();
}
public synchronized void updateFramingRect() {
Point viewResolution = new Point(getWidth(), getHeight());
int width;
int height;
int orientation = DisplayUtils.getScreenOrientation(getContext());
if(mSquareViewFinder) {
if(orientation != Configuration.ORIENTATION_PORTRAIT) {
height = (int) (getHeight() * DEFAULT_SQUARE_DIMENSION_RATIO);
width = height;
} else {
width = (int) (getWidth() * DEFAULT_SQUARE_DIMENSION_RATIO);
height = width;
}
} else {
if(orientation != Configuration.ORIENTATION_PORTRAIT) {
height = (int) (getHeight() * LANDSCAPE_HEIGHT_RATIO);
width = (int) (LANDSCAPE_WIDTH_HEIGHT_RATIO * height);
} else {
width = (int) (getWidth() * PORTRAIT_WIDTH_RATIO);
height = (int) (PORTRAIT_WIDTH_HEIGHT_RATIO * width);
}
}
if(width > getWidth()) {
width = getWidth() - MIN_DIMENSION_DIFF;
}
if(height > getHeight()) {
height = getHeight() - MIN_DIMENSION_DIFF;
}
int leftOffset = (viewResolution.x - width) / 2;
int topOffset = (viewResolution.y - height) / 2;
mFramingRect = new Rect(leftOffset + mViewFinderOffset, topOffset + mViewFinderOffset, leftOffset + width - mViewFinderOffset, topOffset + height - mViewFinderOffset);
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">4</integer>
<integer name="viewfinder_border_length">60</integer>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">5</integer>
<integer name="viewfinder_border_length">80</integer>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">6</integer>
<integer name="viewfinder_border_length">100</integer>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BarcodeScannerView">
<attr name="shouldScaleToFill" format="boolean" />
<attr name="laserEnabled" format="boolean" />
<attr name="laserColor" format="color" />
<attr name="borderColor" format="color" />
<attr name="maskColor" format="color" />
<attr name="borderWidth" format="dimension" />
<attr name="borderLength" format="dimension" />
<attr name="roundedCorner" format="boolean" />
<attr name="cornerRadius" format="dimension" />
<attr name="squaredFinder" format="boolean" />
<attr name="borderAlpha" format="float" />
<attr name="finderOffset" format="dimension" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="viewfinder_mask">#60000000</color>
<color name="viewfinder_laser">#ffcc0000</color>
<color name="viewfinder_border">#ffafed44</color>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="viewfinder_border_width">4</integer>
<integer name="viewfinder_border_length">60</integer>
</resources>

View File

@ -0,0 +1,29 @@
apply plugin: 'com.android.library'
android {
namespace "me.dm7.barcodescanner.zxing"
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
}
compileOptions {
sourceCompatibility versions.sourceCompat
targetCompatibility versions.targetCompat
}
}
dependencies {
api project(":library:external:barcodescanner:core")
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
api 'com.google.zxing:core:3.3.3'
}
afterEvaluate {
tasks.findAll { it.name.startsWith("lint") }.each {
it.enabled = false
}
}

View File

@ -0,0 +1,198 @@
package me.dm7.barcodescanner.zxing;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import me.dm7.barcodescanner.core.BarcodeScannerView;
import me.dm7.barcodescanner.core.DisplayUtils;
public class ZXingScannerView extends BarcodeScannerView {
private static final String TAG = "ZXingScannerView";
public interface ResultHandler {
void handleResult(Result rawResult);
}
private MultiFormatReader mMultiFormatReader;
public static final List<BarcodeFormat> ALL_FORMATS = new ArrayList<>();
private List<BarcodeFormat> mFormats;
private ResultHandler mResultHandler;
static {
ALL_FORMATS.add(BarcodeFormat.AZTEC);
ALL_FORMATS.add(BarcodeFormat.CODABAR);
ALL_FORMATS.add(BarcodeFormat.CODE_39);
ALL_FORMATS.add(BarcodeFormat.CODE_93);
ALL_FORMATS.add(BarcodeFormat.CODE_128);
ALL_FORMATS.add(BarcodeFormat.DATA_MATRIX);
ALL_FORMATS.add(BarcodeFormat.EAN_8);
ALL_FORMATS.add(BarcodeFormat.EAN_13);
ALL_FORMATS.add(BarcodeFormat.ITF);
ALL_FORMATS.add(BarcodeFormat.MAXICODE);
ALL_FORMATS.add(BarcodeFormat.PDF_417);
ALL_FORMATS.add(BarcodeFormat.QR_CODE);
ALL_FORMATS.add(BarcodeFormat.RSS_14);
ALL_FORMATS.add(BarcodeFormat.RSS_EXPANDED);
ALL_FORMATS.add(BarcodeFormat.UPC_A);
ALL_FORMATS.add(BarcodeFormat.UPC_E);
ALL_FORMATS.add(BarcodeFormat.UPC_EAN_EXTENSION);
}
public ZXingScannerView(Context context) {
super(context);
initMultiFormatReader();
}
public ZXingScannerView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
initMultiFormatReader();
}
public void setFormats(List<BarcodeFormat> formats) {
mFormats = formats;
initMultiFormatReader();
}
public void setResultHandler(ResultHandler resultHandler) {
mResultHandler = resultHandler;
}
public Collection<BarcodeFormat> getFormats() {
if(mFormats == null) {
return ALL_FORMATS;
}
return mFormats;
}
private void initMultiFormatReader() {
Map<DecodeHintType,Object> hints = new EnumMap<>(DecodeHintType.class);
hints.put(DecodeHintType.POSSIBLE_FORMATS, getFormats());
mMultiFormatReader = new MultiFormatReader();
mMultiFormatReader.setHints(hints);
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if(mResultHandler == null) {
return;
}
try {
Camera.Parameters parameters = camera.getParameters();
Camera.Size size = parameters.getPreviewSize();
int width = size.width;
int height = size.height;
if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) {
int rotationCount = getRotationCount();
if (rotationCount == 1 || rotationCount == 3) {
int tmp = width;
width = height;
height = tmp;
}
data = getRotatedData(data, camera);
}
Result rawResult = null;
PlanarYUVLuminanceSource source = buildLuminanceSource(data, width, height);
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
rawResult = mMultiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
} catch (NullPointerException npe) {
// This is terrible
} catch (ArrayIndexOutOfBoundsException aoe) {
} finally {
mMultiFormatReader.reset();
}
if (rawResult == null) {
LuminanceSource invertedSource = source.invert();
bitmap = new BinaryBitmap(new HybridBinarizer(invertedSource));
try {
rawResult = mMultiFormatReader.decodeWithState(bitmap);
} catch (NotFoundException e) {
// continue
} finally {
mMultiFormatReader.reset();
}
}
}
final Result finalRawResult = rawResult;
if (finalRawResult != null) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// Stopping the preview can take a little long.
// So we want to set result handler to null to discard subsequent calls to
// onPreviewFrame.
ResultHandler tmpResultHandler = mResultHandler;
mResultHandler = null;
stopCameraPreview();
if (tmpResultHandler != null) {
tmpResultHandler.handleResult(finalRawResult);
}
}
});
} else {
camera.setOneShotPreviewCallback(this);
}
} catch(RuntimeException e) {
// TODO: Terrible hack. It is possible that this method is invoked after camera is released.
Log.e(TAG, e.toString(), e);
}
}
public void resumeCameraPreview(ResultHandler resultHandler) {
mResultHandler = resultHandler;
super.resumeCameraPreview();
}
public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
Rect rect = getFramingRectInPreview(width, height);
if (rect == null) {
return null;
}
// Go ahead and assume it's YUV rather than die.
PlanarYUVLuminanceSource source = null;
try {
source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
rect.width(), rect.height(), false);
} catch(Exception e) {
}
return source;
}
}

View File

@ -58,9 +58,7 @@ dependencies {
implementation libs.airbnb.mavericks
// Span utils
implementation('me.gujun.android:span:1.7') {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation project(":library:external:span")
implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid

View File

@ -0,0 +1,23 @@
apply plugin: 'kotlin'
apply plugin: 'java'
sourceCompatibility = versions.sourceCompat
targetCompatibility = versions.sourceCompat
dependencies {
implementation 'com.squareup:javapoet:1.13.0'
}
task javadocJar(type: Jar, dependsOn: 'javadoc') {
from javadoc.destinationDir
classifier = 'javadoc'
}
task sourcesJar(type: Jar, dependsOn: 'classes') {
from sourceSets.main.allSource
classifier = 'sources'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

View File

@ -0,0 +1,24 @@
package dk.ilios.realmfieldnames
import java.util.TreeMap
/**
* Class responsible for keeping track of the metadata for each Realm model class.
*/
class ClassData(val packageName: String?, val simpleClassName: String, val libraryClass: Boolean = false) {
val fields = TreeMap<String, String?>() // <fieldName, linkedType or null>
fun addField(field: String, linkedType: String?) {
fields.put(field, linkedType)
}
val qualifiedClassName: String
get() {
if (packageName != null && !packageName.isEmpty()) {
return packageName + "." + simpleClassName
} else {
return simpleClassName
}
}
}

View File

@ -0,0 +1,79 @@
package dk.ilios.realmfieldnames
import java.util.Locale
/**
* Class for encapsulating the rules for converting between the field name in the Realm model class
* and the matching name in the "&lt;class&gt;Fields" class.
*/
class FieldNameFormatter {
@JvmOverloads
fun format(fieldName: String?, locale: Locale = Locale.US): String {
if (fieldName == null || fieldName == "") {
return ""
}
// Normalize word separator chars
val normalizedFieldName: String = fieldName.replace('-', '_')
// Iterate field name using the following rules
// lowerCase m followed by upperCase anything is considered hungarian notation
// lowercase char followed by uppercase char is considered camel case
// Two uppercase chars following each other is considered non-standard camelcase
// _ and - are treated as word separators
val result = StringBuilder(normalizedFieldName.length)
if (normalizedFieldName.codePointCount(0, normalizedFieldName.length) == 1) {
result.append(normalizedFieldName)
} else {
var previousCodepoint: Int?
var currentCodepoint: Int? = null
val length = normalizedFieldName.length
var offset = 0
while (offset < length) {
previousCodepoint = currentCodepoint
currentCodepoint = normalizedFieldName.codePointAt(offset)
if (previousCodepoint != null) {
if (Character.isUpperCase(currentCodepoint) &&
!Character.isUpperCase(previousCodepoint) &&
previousCodepoint === 'm'.code as Int? &&
result.length == 1
) {
// Hungarian notation starting with: mX
result.delete(0, 1)
result.appendCodePoint(currentCodepoint)
} else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) {
// InvalidCamelCase: XXYx (should have been xxYx)
if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) {
val nextCodePoint = normalizedFieldName.codePointAt(offset + Character.charCount(currentCodepoint))
if (Character.isLowerCase(nextCodePoint)) {
result.append("_")
}
}
result.appendCodePoint(currentCodepoint)
} else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) {
// Word-separator: x-x or x_x
result.append("_")
} else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit(
previousCodepoint
)) {
// camelCase: xX
result.append("_")
result.appendCodePoint(currentCodepoint)
} else {
// Unknown type
result.appendCodePoint(currentCodepoint)
}
} else {
// Only triggered for first code point
result.appendCodePoint(currentCodepoint)
}
offset += Character.charCount(currentCodepoint)
}
}
return result.toString().uppercase(locale)
}
}

View File

@ -0,0 +1,77 @@
package dk.ilios.realmfieldnames
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeSpec
import java.io.IOException
import javax.annotation.processing.Filer
import javax.lang.model.element.Modifier
/**
* Class responsible for creating the final output files.
*/
class FileGenerator(private val filer: Filer) {
private val formatter: FieldNameFormatter
init {
this.formatter = FieldNameFormatter()
}
/**
* Generates all the "&lt;class&gt;Fields" fields with field name references.
* @param fileData Files to create.
* *
* @return `true` if the files where generated, `false` if not.
*/
fun generate(fileData: Set<ClassData>): Boolean {
return fileData
.filter { !it.libraryClass }
.all { generateFile(it, fileData) }
}
private fun generateFile(classData: ClassData, classPool: Set<ClassData>): Boolean {
val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n",
classData.packageName, classData.simpleClassName)
// Add a static field reference to each queryable field in the Realm model class
classData.fields.forEach { fieldName, value ->
if (value != null) {
// Add linked field names (only up to depth 1)
for (data in classPool) {
if (data.qualifiedClassName == value) {
val linkedTypeSpec = TypeSpec.classBuilder(formatter.format(fieldName))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
val linkedClassFields = data.fields
addField(linkedTypeSpec, "$", fieldName)
for (linkedFieldName in linkedClassFields.keys) {
addField(linkedTypeSpec, linkedFieldName, fieldName + "." + linkedFieldName)
}
fileBuilder.addType(linkedTypeSpec.build())
}
}
} else {
// Add normal field name
addField(fileBuilder, fieldName, fieldName)
}
}
val javaFile = JavaFile.builder(classData.packageName, fileBuilder.build()).build()
try {
javaFile.writeTo(filer)
return true
} catch (e: IOException) {
// e.printStackTrace()
return false
}
}
private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) {
val field = FieldSpec.builder(String::class.java, formatter.format(fieldName))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("\$S", fieldNameValue)
.build()
fileBuilder.addField(field)
}
}

View File

@ -0,0 +1,197 @@
package dk.ilios.realmfieldnames
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Messager
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedAnnotationTypes
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.Modifier
import javax.lang.model.element.PackageElement
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.TypeMirror
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic
/**
* The Realm Field Names Generator is a processor that looks at all available Realm model classes
* and create an companion class with easy, type-safe access to all field names.
*/
@SupportedAnnotationTypes("io.realm.annotations.RealmClass")
class RealmFieldNamesProcessor : AbstractProcessor() {
private val classes = HashSet<ClassData>()
private lateinit var typeUtils: Types
private lateinit var messager: Messager
private lateinit var elementUtils: Elements
private var ignoreAnnotation: TypeMirror? = null
private var realmClassAnnotation: TypeElement? = null
private var realmModelInterface: TypeMirror? = null
private var realmListClass: DeclaredType? = null
private var realmResultsClass: DeclaredType? = null
private var fileGenerator: FileGenerator? = null
private var done = false
@Synchronized
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
typeUtils = processingEnv.typeUtils!!
messager = processingEnv.messager!!
elementUtils = processingEnv.elementUtils!!
// If the Realm class isn't found something is wrong the project setup.
// Most likely Realm isn't on the class path, so just disable the
// annotation processor
val isRealmAvailable = elementUtils.getTypeElement("io.realm.Realm") != null
if (!isRealmAvailable) {
done = true
} else {
ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType()
realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass")
realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType()
realmListClass = typeUtils.getDeclaredType(
elementUtils.getTypeElement("io.realm.RealmList"),
typeUtils.getWildcardType(null, null)
)
realmResultsClass = typeUtils.getDeclaredType(
elementUtils.getTypeElement("io.realm.RealmResults"),
typeUtils.getWildcardType(null, null)
)
fileGenerator = FileGenerator(processingEnv.filer)
}
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
if (done) {
return CONSUME_ANNOTATIONS
}
// Create all proxy classes
roundEnv.getElementsAnnotatedWith(realmClassAnnotation).forEach { classElement ->
if (typeUtils.isAssignable(classElement.asType(), realmModelInterface)) {
val classData = processClass(classElement as TypeElement)
classes.add(classData)
}
}
// If a model class references a library class, the library class will not be part of this
// annotation processor round. For all those references we need to pull field information
// from the classpath instead.
val libraryClasses = HashMap<String, ClassData>()
classes.forEach {
it.fields.forEach { _, value ->
// Analyze the library class file the first time it is encountered.
if (value != null) {
if (classes.all { it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) {
libraryClasses.put(value, processLibraryClass(value))
}
}
}
}
classes.addAll(libraryClasses.values)
done = fileGenerator!!.generate(classes)
return CONSUME_ANNOTATIONS
}
private fun processClass(classElement: TypeElement): ClassData {
val packageName = getPackageName(classElement)
val className = classElement.simpleName.toString()
val data = ClassData(packageName, className)
// Find all appropriate fields
classElement.enclosedElements.forEach {
val elementKind = it.kind
if (elementKind == ElementKind.FIELD) {
val variableElement = it as VariableElement
val modifiers = variableElement.modifiers
if (modifiers.contains(Modifier.STATIC)) {
return@forEach // completely ignore any static fields
}
// Don't add any fields marked with @Ignore
val ignoreField = variableElement.annotationMirrors
.map { it.annotationType.toString() }
.contains("io.realm.annotations.Ignore")
if (!ignoreField) {
data.addField(it.getSimpleName().toString(), getLinkedFieldType(it))
}
}
}
return data
}
private fun processLibraryClass(qualifiedClassName: String): ClassData {
val libraryClass = Class.forName(qualifiedClassName) // Library classes should be on the classpath
val packageName = libraryClass.`package`.name
val className = libraryClass.simpleName
val data = ClassData(packageName, className, libraryClass = true)
libraryClass.declaredFields.forEach { field ->
if (java.lang.reflect.Modifier.isStatic(field.modifiers)) {
return@forEach // completely ignore any static fields
}
// Add field if it is not being ignored.
if (field.annotations.all { it.toString() != "io.realm.annotations.Ignore" }) {
data.addField(field.name, field.type.name)
}
}
return data
}
/**
* Returns the qualified name of the linked Realm class field or `null` if it is not a linked
* class.
*/
private fun getLinkedFieldType(field: Element): String? {
if (typeUtils.isAssignable(field.asType(), realmModelInterface)) {
// Object link
val typeElement = elementUtils.getTypeElement(field.asType().toString())
return typeElement.qualifiedName.toString()
} else if (typeUtils.isAssignable(field.asType(), realmListClass) || typeUtils.isAssignable(field.asType(), realmResultsClass)) {
// List link or LinkingObjects
val fieldType = field.asType()
val typeArguments = (fieldType as DeclaredType).typeArguments
if (typeArguments.size == 0) {
return null
}
return typeArguments[0].toString()
} else {
return null
}
}
private fun getPackageName(classElement: TypeElement): String? {
val enclosingElement = classElement.enclosingElement
if (enclosingElement.kind != ElementKind.PACKAGE) {
messager.printMessage(
Diagnostic.Kind.ERROR,
"Could not determine the package name. Enclosing element was: " + enclosingElement.kind
)
return null
}
val packageElement = enclosingElement as PackageElement
return packageElement.qualifiedName.toString()
}
companion object {
private const val CONSUME_ANNOTATIONS = false
}
}

View File

@ -0,0 +1 @@
dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating

View File

@ -0,0 +1 @@
dk.ilios.realmfieldnames.RealmFieldNamesProcessor

20
library/external/span/build.gradle vendored Normal file
View File

@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "me.gujun.android.span"
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
implementation 'com.android.support:support-annotations:28.0.0'
}

View File

@ -0,0 +1,316 @@
package me.gujun.android.span
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Layout
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.AbsoluteSizeSpan
import android.text.style.AlignmentSpan
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.QuoteSpan
import android.text.style.StyleSpan
import android.text.style.SubscriptSpan
import android.text.style.SuperscriptSpan
import android.text.style.TypefaceSpan
import android.text.style.URLSpan
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.Dimension
import me.gujun.android.span.style.CustomTypefaceSpan
import me.gujun.android.span.style.LineSpacingSpan
import me.gujun.android.span.style.SimpleClickableSpan
import me.gujun.android.span.style.TextDecorationLineSpan
import me.gujun.android.span.style.VerticalPaddingSpan
class Span(val parent: Span? = null) : SpannableStringBuilder() {
companion object {
val EMPTY_STYLE = Span()
var globalStyle: Span = EMPTY_STYLE
}
var text: CharSequence = ""
@ColorInt var textColor: Int? = parent?.textColor
@ColorInt var backgroundColor: Int? = parent?.backgroundColor
@Dimension(unit = Dimension.PX) var textSize: Int? = parent?.textSize
var fontFamily: String? = parent?.fontFamily
var typeface: Typeface? = parent?.typeface
var textStyle: String? = parent?.textStyle
var alignment: String? = parent?.alignment
var textDecorationLine: String? = parent?.textDecorationLine
@Dimension(unit = Dimension.PX) var lineSpacing: Int? = null
@Dimension(unit = Dimension.PX) var paddingTop: Int? = null
@Dimension(unit = Dimension.PX) var paddingBottom: Int? = null
@Dimension(unit = Dimension.PX) var verticalPadding: Int? = null
var onClick: (() -> Unit)? = null
var spans: ArrayList<Any> = ArrayList()
var style: Span = EMPTY_STYLE
private fun buildCharacterStyle(builder: ArrayList<Any>) {
if (textColor != null) {
builder.add(ForegroundColorSpan(textColor!!))
}
if (backgroundColor != null) {
builder.add(BackgroundColorSpan(backgroundColor!!))
}
if (textSize != null) {
builder.add(AbsoluteSizeSpan(textSize!!))
}
if (!TextUtils.isEmpty(fontFamily)) {
builder.add(TypefaceSpan(fontFamily))
}
if (typeface != null) {
builder.add(CustomTypefaceSpan(typeface!!))
}
if (!TextUtils.isEmpty(textStyle)) {
builder.add(StyleSpan(when (textStyle) {
"normal" -> Typeface.NORMAL
"bold" -> Typeface.BOLD
"italic" -> Typeface.ITALIC
"bold_italic" -> Typeface.BOLD_ITALIC
else -> throw RuntimeException("Unknown text style")
}))
}
if (!TextUtils.isEmpty(textDecorationLine)) {
builder.add(TextDecorationLineSpan(textDecorationLine!!))
}
if (onClick != null) {
builder.add(object : SimpleClickableSpan() {
override fun onClick(widget: View) {
onClick?.invoke()
}
})
}
}
private fun buildParagraphStyle(builder: ArrayList<Any>) {
if (!TextUtils.isEmpty(alignment)) {
builder.add(AlignmentSpan.Standard(when (alignment) {
"normal" -> Layout.Alignment.ALIGN_NORMAL
"opposite" -> Layout.Alignment.ALIGN_OPPOSITE
"center" -> Layout.Alignment.ALIGN_CENTER
else -> throw RuntimeException("Unknown text alignment")
}))
}
if (lineSpacing != null) {
builder.add(LineSpacingSpan(lineSpacing!!))
}
paddingTop = when {
paddingTop != null -> paddingTop
verticalPadding != null -> verticalPadding
else -> 0
}
paddingBottom = when {
paddingBottom != null -> paddingBottom
verticalPadding != null -> verticalPadding
else -> 0
}
if (paddingTop != 0 || paddingBottom != 0) {
builder.add(VerticalPaddingSpan(paddingTop!!, paddingBottom!!))
}
}
private fun prebuild() {
override(style)
}
fun build(): Span {
prebuild()
val builder = ArrayList<Any>()
if (!TextUtils.isEmpty(text)) {
var p = this.parent
while (p != null) {
if (!TextUtils.isEmpty(p.text)) {
throw RuntimeException("Can't nest \"$text\" in spans")
}
p = p.parent
}
append(text)
buildCharacterStyle(builder)
buildParagraphStyle(builder)
} else {
buildParagraphStyle(builder)
}
builder.addAll(spans)
builder.forEach {
setSpan(it, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
}
fun override(style: Span) {
if (textColor == null) {
textColor = style.textColor
}
if (backgroundColor == null) {
backgroundColor = style.backgroundColor
}
if (textSize == null) {
textSize = style.textSize
}
if (fontFamily == null) {
fontFamily = style.fontFamily
}
if (typeface == null) {
typeface = style.typeface
}
if (textStyle == null) {
textStyle = style.textStyle
}
if (alignment == null) {
alignment = style.alignment
}
if (textDecorationLine == null) {
textDecorationLine = style.textDecorationLine
}
if (lineSpacing == null) {
lineSpacing = style.lineSpacing
}
if (paddingTop == null) {
paddingTop = style.paddingTop
}
if (paddingBottom == null) {
paddingBottom = style.paddingBottom
}
if (verticalPadding == null) {
verticalPadding = style.verticalPadding
}
if (onClick == null) {
onClick = style.onClick
}
spans.addAll(style.spans)
}
operator fun CharSequence.unaryPlus(): CharSequence {
return append(Span(parent = this@Span).apply {
text = this@unaryPlus
build()
})
}
operator fun Span.plus(other: CharSequence): CharSequence {
return append(Span(parent = this).apply {
text = other
build()
})
}
}
fun span(init: Span.() -> Unit): Span = Span().apply {
override(Span.globalStyle)
init()
build()
}
fun span(text: CharSequence, init: Span.() -> Unit): Span = Span().apply {
override(Span.globalStyle)
this.text = text
init()
build()
}
fun style(init: Span.() -> Unit): Span = Span().apply {
init()
}
fun Span.span(init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
init()
build()
})
}
fun Span.span(text: CharSequence, init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
init()
build()
})
}
fun Span.link(url: String, text: CharSequence = "",
init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(URLSpan(url))
init()
build()
})
}
fun Span.quote(@ColorInt color: Int, text: CharSequence = "",
init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(QuoteSpan(color))
init()
build()
})
}
fun Span.superscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(SuperscriptSpan())
init()
build()
})
}
fun Span.subscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(SubscriptSpan())
init()
build()
})
}
fun Span.image(drawable: Drawable, alignment: String = "bottom",
init: Span.() -> Unit = {}): Span = apply {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
append(Span(parent = this).apply {
this.text = " "
this.spans.add(ImageSpan(drawable, when (alignment) {
"bottom" -> ImageSpan.ALIGN_BOTTOM
"baseline" -> ImageSpan.ALIGN_BASELINE
else -> throw RuntimeException("Unknown image alignment")
}))
init()
build()
})
}
fun Span.addSpan(what: Any) = apply {
this.spans.add(what)
}

View File

@ -0,0 +1,36 @@
package me.gujun.android.span.style
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
class CustomTypefaceSpan(private val tf: Typeface) : MetricAffectingSpan() {
override fun updateMeasureState(paint: TextPaint) {
apply(paint, tf)
}
override fun updateDrawState(ds: TextPaint) {
apply(ds, tf)
}
private fun apply(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.typeface
oldStyle = old?.style ?: 0
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.isFakeBoldText = true
}
if (fake and Typeface.ITALIC != 0) {
paint.textSkewX = -0.25f
}
paint.typeface = tf
}
}

View File

@ -0,0 +1,31 @@
package me.gujun.android.span.style
import android.graphics.Paint.FontMetricsInt
import android.text.Spanned
import android.text.style.LineHeightSpan
class LineSpacingSpan(private val add: Int) : LineHeightSpan {
override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int,
fm: FontMetricsInt) {
text as Spanned
/*val spanStart =*/ text.getSpanStart(this)
val spanEnd = text.getSpanEnd(this)
// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character
// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#"))
// Log.d("DEBUG", "LineSpacingSpan: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv")
// Log.d("DEBUG", "$fm")
// Log.d("DEBUG", "-----------------------")
if (spanstartv == v) {
fm.descent += add
} else if (text[start - 1] == '\n') {
fm.descent += add
}
if (end == spanEnd || end - 1 == spanEnd) {
fm.descent -= add
}
}
}

View File

@ -0,0 +1,10 @@
package me.gujun.android.span.style
import android.text.TextPaint
import android.text.style.ClickableSpan
abstract class SimpleClickableSpan : ClickableSpan() {
override fun updateDrawState(ds: TextPaint) {
// no-op
}
}

View File

@ -0,0 +1,29 @@
package me.gujun.android.span.style
import android.text.TextPaint
import android.text.style.CharacterStyle
class TextDecorationLineSpan(private val textDecorationLine: String) : CharacterStyle() {
override fun updateDrawState(tp: TextPaint) {
when (textDecorationLine) {
"none" -> {
tp.isUnderlineText = false
tp.isStrikeThruText = false
}
"underline" -> {
tp.isUnderlineText = true
tp.isStrikeThruText = false
}
"line-through" -> {
tp.isUnderlineText = false
tp.isStrikeThruText = true
}
"underline line-through" -> {
tp.isUnderlineText = true
tp.isStrikeThruText = true
}
else -> throw RuntimeException("Unknown text decoration line")
}
}
}

View File

@ -0,0 +1,41 @@
package me.gujun.android.span.style
import android.graphics.Paint.FontMetricsInt
import android.text.Spanned
import android.text.style.LineHeightSpan
class VerticalPaddingSpan(private val paddingTop: Int,
private val paddingBottom: Int) : LineHeightSpan {
private var flag: Boolean = true
override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int,
fm: FontMetricsInt) {
text as Spanned
/*val spanStart =*/ text.getSpanStart(this)
val spanEnd = text.getSpanEnd(this)
// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character
// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#"))
// Log.d("DEBUG", "VerticalPadding: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv")
// Log.d("DEBUG", "$fm")
// Log.d("DEBUG", "-----------------------")
if (spanstartv == v) {
fm.top -= paddingTop
fm.ascent -= paddingTop
flag = true
} else if (flag && text[start - 1] != '\n') {
fm.top += paddingTop
fm.ascent += paddingTop
flag = false
} else {
flag = false
}
if (end == spanEnd || end - 1 == spanEnd) {
fm.descent += paddingBottom
fm.bottom += paddingBottom
}
}
}

View File

@ -0,0 +1,25 @@
apply plugin: 'com.android.library'
apply plugin: 'com.android.library'
android {
namespace "com.amulyakhare.textdrawable"
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
}
compileOptions {
sourceCompatibility versions.sourceCompat
targetCompatibility versions.targetCompat
}
}
afterEvaluate {
tasks.findAll { it.name.startsWith("lint") }.each {
it.enabled = false
}
}

View File

@ -0,0 +1,316 @@
package com.amulyakhare.textdrawable;
import android.graphics.*;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.RoundRectShape;
/**
* @author amulya
* @datetime 14 Oct 2014, 3:53 PM
*/
public class TextDrawable extends ShapeDrawable {
private final Paint textPaint;
private final Paint borderPaint;
private static final float SHADE_FACTOR = 0.9f;
private final String text;
private final int color;
private final RectShape shape;
private final int height;
private final int width;
private final int fontSize;
private final float radius;
private final int borderThickness;
private TextDrawable(Builder builder) {
super(builder.shape);
// shape properties
shape = builder.shape;
height = builder.height;
width = builder.width;
radius = builder.radius;
// text and color
text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text;
color = builder.color;
// text paint settings
fontSize = builder.fontSize;
textPaint = new Paint();
textPaint.setColor(builder.textColor);
textPaint.setAntiAlias(true);
textPaint.setFakeBoldText(builder.isBold);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTypeface(builder.font);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setStrokeWidth(builder.borderThickness);
// border paint settings
borderThickness = builder.borderThickness;
borderPaint = new Paint();
borderPaint.setColor(getDarkerShade(color));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderThickness);
// drawable paint color
Paint paint = getPaint();
paint.setColor(color);
}
private int getDarkerShade(int color) {
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
(int)(SHADE_FACTOR * Color.green(color)),
(int)(SHADE_FACTOR * Color.blue(color)));
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Rect r = getBounds();
// draw border
if (borderThickness > 0) {
drawBorder(canvas);
}
int count = canvas.save();
canvas.translate(r.left, r.top);
// draw text
int width = this.width < 0 ? r.width() : this.width;
int height = this.height < 0 ? r.height() : this.height;
int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize;
textPaint.setTextSize(fontSize);
canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
canvas.restoreToCount(count);
}
private void drawBorder(Canvas canvas) {
RectF rect = new RectF(getBounds());
rect.inset(borderThickness/2, borderThickness/2);
if (shape instanceof OvalShape) {
canvas.drawOval(rect, borderPaint);
}
else if (shape instanceof RoundRectShape) {
canvas.drawRoundRect(rect, radius, radius, borderPaint);
}
else {
canvas.drawRect(rect, borderPaint);
}
}
@Override
public void setAlpha(int alpha) {
textPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
textPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
public static IShapeBuilder builder() {
return new Builder();
}
public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder {
private String text;
private int color;
private int borderThickness;
private int width;
private int height;
private Typeface font;
private RectShape shape;
public int textColor;
private int fontSize;
private boolean isBold;
private boolean toUpperCase;
public float radius;
private Builder() {
text = "";
color = Color.GRAY;
textColor = Color.WHITE;
borderThickness = 0;
width = -1;
height = -1;
shape = new RectShape();
font = Typeface.create("sans-serif-light", Typeface.NORMAL);
fontSize = -1;
isBold = false;
toUpperCase = false;
}
public IConfigBuilder width(int width) {
this.width = width;
return this;
}
public IConfigBuilder height(int height) {
this.height = height;
return this;
}
public IConfigBuilder textColor(int color) {
this.textColor = color;
return this;
}
public IConfigBuilder withBorder(int thickness) {
this.borderThickness = thickness;
return this;
}
public IConfigBuilder useFont(Typeface font) {
this.font = font;
return this;
}
public IConfigBuilder fontSize(int size) {
this.fontSize = size;
return this;
}
public IConfigBuilder bold() {
this.isBold = true;
return this;
}
public IConfigBuilder toUpperCase() {
this.toUpperCase = true;
return this;
}
@Override
public IConfigBuilder beginConfig() {
return this;
}
@Override
public IShapeBuilder endConfig() {
return this;
}
@Override
public IBuilder rect() {
this.shape = new RectShape();
return this;
}
@Override
public IBuilder round() {
this.shape = new OvalShape();
return this;
}
@Override
public IBuilder roundRect(int radius) {
this.radius = radius;
float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius};
this.shape = new RoundRectShape(radii, null, null);
return this;
}
@Override
public TextDrawable buildRect(String text, int color) {
rect();
return build(text, color);
}
@Override
public TextDrawable buildRoundRect(String text, int color, int radius) {
roundRect(radius);
return build(text, color);
}
@Override
public TextDrawable buildRound(String text, int color) {
round();
return build(text, color);
}
@Override
public TextDrawable build(String text, int color) {
this.color = color;
this.text = text;
return new TextDrawable(this);
}
}
public interface IConfigBuilder {
public IConfigBuilder width(int width);
public IConfigBuilder height(int height);
public IConfigBuilder textColor(int color);
public IConfigBuilder withBorder(int thickness);
public IConfigBuilder useFont(Typeface font);
public IConfigBuilder fontSize(int size);
public IConfigBuilder bold();
public IConfigBuilder toUpperCase();
public IShapeBuilder endConfig();
}
public static interface IBuilder {
public TextDrawable build(String text, int color);
}
public static interface IShapeBuilder {
public IConfigBuilder beginConfig();
public IBuilder rect();
public IBuilder round();
public IBuilder roundRect(int radius);
public TextDrawable buildRect(String text, int color);
public TextDrawable buildRoundRect(String text, int color, int radius);
public TextDrawable buildRound(String text, int color);
}
}

View File

@ -189,7 +189,7 @@ dependencies {
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
kapt project(":library:external:realmfieldnameshelper")
// Shared Preferences
implementation libs.androidx.preferenceKtx

View File

@ -11,6 +11,12 @@ include ':library:multipicker'
include ':library:external:jsonviewer'
include ':library:external:diff-match-patch'
include ':library:external:dialpad'
include ':library:external:textdrawable'
include ':library:external:autocomplete'
include ':library:external:realmfieldnameshelper'
include ':library:external:span'
include ':library:external:barcodescanner:core'
include ':library:external:barcodescanner:zxing'
include ':library:rustCrypto'
include ':matrix-sdk-android'

View File

@ -396,6 +396,7 @@ dependencies {
implementation project(':vector')
implementation project(':vector-config')
implementation project(':library:core-utils')
debugImplementation project(':library:external:span')
debugImplementation project(':library:ui-styles')
implementation libs.dagger.hilt
implementation 'androidx.multidex:multidex:2.0.1'

View File

@ -116,6 +116,8 @@ dependencies {
implementation project(":matrix-sdk-android-flow")
implementation project(":library:external:jsonviewer")
implementation project(":library:external:diff-match-patch")
implementation project(":library:external:textdrawable")
implementation project(":library:external:autocomplete")
implementation project(":library:ui-strings")
implementation project(":library:ui-styles")
implementation project(":library:core-utils")
@ -184,11 +186,8 @@ dependencies {
api libs.androidx.preferenceKtx
// UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation libs.google.material
api('me.gujun.android:span:1.7') {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation project(":library:external:span")
implementation libs.markwon.core
implementation libs.markwon.extLatex
implementation libs.markwon.imageGlide
@ -210,8 +209,6 @@ dependencies {
// Alerter
implementation 'com.github.tapadoo:alerter:7.2.4'
implementation 'com.otaliastudios:autocomplete:1.1.0'
// Shake detection
implementation 'com.squareup:seismic:1.0.3'
@ -266,11 +263,7 @@ dependencies {
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3'
// Excludes the legacy support library annotation usages
// https://github.com/dm77/barcodescanner/blob/d036996c8a6f36a68843ffe539c834c28944b2d5/core/src/main/java/me/dm7/barcodescanner/core/CameraWrapper.java#L4
implementation ('me.dm7.barcodescanner:zxing:1.9.13') {
exclude group: 'com.android.support', module: 'support-v4'
}
implementation project(":library:external:barcodescanner:zxing")
// Emoji Keyboard
api libs.vanniktech.emojiMaterial

View File

@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.AutocompletePresenter
abstract class RecyclerViewPresenter<T>(context: Context?) : AutocompletePresenter<T>(context) {
abstract class RecyclerViewPresenter<T : Any>(context: Context) : AutocompletePresenter<T>(context) {
private var recyclerView: RecyclerView? = null
private var clicks: ClickProvider<T>? = null

View File

@ -32,16 +32,15 @@ class CommandAutocompletePolicy @Inject constructor() : AutocompletePolicy {
return ""
}
override fun onDismiss(text: Spannable?) {
override fun onDismiss(text: Spannable) {
}
// Only if text which starts with '/' and without space
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
return enabled && text?.startsWith("/") == true &&
!text.contains(" ")
override fun shouldShowPopup(text: Spannable, cursorPos: Int): Boolean {
return enabled && text.startsWith("/") && !text.contains(" ")
}
override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean {
override fun shouldDismissPopup(text: Spannable, cursorPos: Int): Boolean {
return !shouldShowPopup(text, cursorPos)
}
}