Typefaced views logic in release candidate state
This commit is contained in:
parent
2bd7e1e967
commit
45d39b2df8
|
|
@ -21,6 +21,7 @@ package ru.touchin.roboswag.components.navigation.fragments;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Parcel;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
|
@ -58,15 +59,13 @@ public abstract class ViewControllerFragment<TState extends AbstractState, TActi
|
|||
|
||||
private static final String VIEW_CONTROLLER_STATE_EXTRA = "VIEW_CONTROLLER_STATE_EXTRA";
|
||||
|
||||
private static boolean isInDebugMode;
|
||||
private static boolean inDebugMode;
|
||||
|
||||
/**
|
||||
* Enables debugging features like serialization of {@link #getState()} every creation.
|
||||
*
|
||||
* @param isInDebugMode True if such fragments should work in debug mode.
|
||||
*/
|
||||
public static void setIsInDebugMode(final boolean isInDebugMode) {
|
||||
ViewControllerFragment.isInDebugMode = isInDebugMode;
|
||||
public static void setInDebugMode() {
|
||||
inDebugMode = true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
@ -134,7 +133,7 @@ public abstract class ViewControllerFragment<TState extends AbstractState, TActi
|
|||
? (TState) savedInstanceState.getSerializable(VIEW_CONTROLLER_STATE_EXTRA)
|
||||
: (getArguments() != null ? (TState) getArguments().getSerializable(VIEW_CONTROLLER_STATE_EXTRA) : null);
|
||||
if (state != null) {
|
||||
if (isInDebugMode) {
|
||||
if (inDebugMode) {
|
||||
state = reserialize(state);
|
||||
}
|
||||
state.onCreate();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StyleableRes;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 18/07/2014.
|
||||
* Manager for typefaces stored in 'assets/fonts' folder.
|
||||
*/
|
||||
public final class Typefaces {
|
||||
|
||||
private static final Map<String, Typeface> TYPEFACES_MAP = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Returns {@link Typeface} by name from assets 'fonts' folder.
|
||||
*
|
||||
* @param context Context of assets where typeface file stored in;
|
||||
* @param name Full name of typeface (without extension, e.g. 'Roboto-Regular');
|
||||
* @return {@link Typeface} from assets.
|
||||
*/
|
||||
@NonNull
|
||||
public static Typeface getByName(@NonNull final Context context, @NonNull final String name) {
|
||||
synchronized (TYPEFACES_MAP) {
|
||||
Typeface result = TYPEFACES_MAP.get(name);
|
||||
if (result == null) {
|
||||
final AssetManager assetManager = context.getAssets();
|
||||
result = Typeface.DEFAULT;
|
||||
try {
|
||||
final List<String> fonts = Arrays.asList(assetManager.list("fonts"));
|
||||
if (fonts.contains(name + ".ttf")) {
|
||||
result = Typeface.createFromAsset(assetManager, "fonts/" + name + ".ttf");
|
||||
} else if (fonts.contains(name + ".otf")) {
|
||||
result = Typeface.createFromAsset(assetManager, "fonts/" + name + ".otf");
|
||||
} else {
|
||||
Lc.assertion("Can't find .otf or .ttf file in assets folder 'fonts' with name: " + name);
|
||||
}
|
||||
} catch (final IOException exception) {
|
||||
Lc.assertion(new ShouldNotHappenException("Can't get font " + name + '.'
|
||||
+ "Did you forget to create assets folder named 'fonts'?", exception));
|
||||
}
|
||||
TYPEFACES_MAP.put(name, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link Typeface} by name from assets 'fonts' folder.
|
||||
*
|
||||
* @param context Context of assets where typeface file stored in;
|
||||
* @param attrs Attributes of view to get font from;
|
||||
* @param styleableId Id of attribute set (e.g. {@link ru.touchin.roboswag.components.R.styleable#TypefacedTextView});
|
||||
* @param attributeId Id of attribute (e.g. {@link ru.touchin.roboswag.components.R.styleable#TypefacedTextView_customTypeface});
|
||||
* @return {@link Typeface} from assets.
|
||||
*/
|
||||
@NonNull
|
||||
public static Typeface getFromAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs,
|
||||
@StyleableRes final int[] styleableId, @StyleableRes final int attributeId) {
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(attrs, styleableId);
|
||||
final String customTypeface = typedArray.getString(attributeId);
|
||||
typedArray.recycle();
|
||||
if (customTypeface != null) {
|
||||
return getByName(context, customTypeface);
|
||||
}
|
||||
Lc.w("Couldn't find custom typeface. Returns default");
|
||||
return Typeface.DEFAULT;
|
||||
}
|
||||
|
||||
private Typefaces() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
/*
|
||||
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Display;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import rx.functions.Action0;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 13/11/2015.
|
||||
* General utilities related to UI (Inflation, Views, Metrics, Activities etc.).
|
||||
*/
|
||||
public final class UiUtils {
|
||||
|
||||
/**
|
||||
* Delay to let user view rippleeffect before screen changed.
|
||||
*/
|
||||
public static final long RIPPLE_EFFECT_DELAY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 150 : 0;
|
||||
|
||||
private static final Handler RIPPLE_HANDLER = new Handler(Looper.getMainLooper());
|
||||
|
||||
/**
|
||||
* Method to inflate view with right layout parameters based on container and add inflated view as a child to it.
|
||||
*
|
||||
* @param layoutId Id of layout resource;
|
||||
* @param parent Container to rightly resolve layout parameters of view in XML;
|
||||
* @return Inflated view.
|
||||
*/
|
||||
@NonNull
|
||||
public static View inflateAndAdd(@LayoutRes final int layoutId, @NonNull final ViewGroup parent) {
|
||||
LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, true);
|
||||
return parent.getChildAt(parent.getChildCount() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to inflate view with right layout parameters based on container but not adding inflated view as a child to it.
|
||||
*
|
||||
* @param layoutId Id of layout resource;
|
||||
* @param parent Container to rightly resolve layout parameters of view in XML;
|
||||
* @return Inflated view.
|
||||
*/
|
||||
@NonNull
|
||||
public static View inflate(@LayoutRes final int layoutId, @NonNull final ViewGroup parent) {
|
||||
return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets click listener to view. On click it will call something after delay.
|
||||
*
|
||||
* @param targetView View to set click listener to;
|
||||
* @param onClickListener Click listener;
|
||||
* @param delay Delay after which click listener will be called.
|
||||
*/
|
||||
public static void setOnRippleClickListener(@NonNull final View targetView, @Nullable final Action0 onClickListener, final long delay) {
|
||||
setOnRippleClickListener(targetView, onClickListener != null ? v -> onClickListener.call() : null, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets click listener to view. On click it will call something with {@link #RIPPLE_EFFECT_DELAY}.
|
||||
*
|
||||
* @param targetView View to set click listener to;
|
||||
* @param onClickListener Click listener.
|
||||
*/
|
||||
public static void setOnRippleClickListener(@NonNull final View targetView, @Nullable final Action0 onClickListener) {
|
||||
setOnRippleClickListener(targetView, onClickListener != null ? v -> onClickListener.call() : null, RIPPLE_EFFECT_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets click listener to view. On click it will call something with {@link #RIPPLE_EFFECT_DELAY}.
|
||||
*
|
||||
* @param targetView View to set click listener to;
|
||||
* @param onClickListener Click listener.
|
||||
*/
|
||||
public static void setOnRippleClickListener(@NonNull final View targetView, @Nullable final View.OnClickListener onClickListener) {
|
||||
setOnRippleClickListener(targetView, onClickListener, RIPPLE_EFFECT_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets click listener to view. On click it will call something after delay.
|
||||
*
|
||||
* @param targetView View to set click listener to;
|
||||
* @param onClickListener Click listener;
|
||||
* @param delay Delay after which click listener will be called.
|
||||
*/
|
||||
public static void setOnRippleClickListener(@NonNull final View targetView,
|
||||
@Nullable final View.OnClickListener onClickListener,
|
||||
final long delay) {
|
||||
if (onClickListener == null) {
|
||||
targetView.setOnClickListener(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final Runnable runnable = () -> {
|
||||
if (targetView.getWindowVisibility() == View.VISIBLE) {
|
||||
onClickListener.onClick(targetView);
|
||||
}
|
||||
};
|
||||
|
||||
targetView.setOnClickListener(v -> {
|
||||
RIPPLE_HANDLER.removeCallbacksAndMessages(null);
|
||||
RIPPLE_HANDLER.postDelayed(runnable, delay);
|
||||
});
|
||||
}
|
||||
|
||||
private UiUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities methods related to metrics.
|
||||
*/
|
||||
public static class OfMetrics {
|
||||
|
||||
private static final int MAX_METRICS_TRIES_COUNT = 5;
|
||||
|
||||
/**
|
||||
* Returns right metrics with non-zero height/width.
|
||||
* It is common bug when metrics are calling at {@link Application#onCreate()} method and it returns metrics with zero height/width.
|
||||
*
|
||||
* @param context {@link Context} of metrics;
|
||||
* @return {@link DisplayMetrics}.
|
||||
*/
|
||||
@SuppressWarnings("BusyWait")
|
||||
@NonNull
|
||||
public static DisplayMetrics getDisplayMetrics(@NonNull final Context context) {
|
||||
DisplayMetrics result = context.getResources().getDisplayMetrics();
|
||||
// it is needed to avoid bug with invalid metrics when user restore application from other application
|
||||
int metricsTryNumber = 0;
|
||||
while (metricsTryNumber < MAX_METRICS_TRIES_COUNT && (result.heightPixels <= 0 || result.widthPixels <= 0)) {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (final InterruptedException ignored) {
|
||||
return result;
|
||||
}
|
||||
result = context.getResources().getDisplayMetrics();
|
||||
metricsTryNumber++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply converts DP to pixels.
|
||||
*
|
||||
* @param context {@link Context} of metrics;
|
||||
* @param sizeInDp Size in DP;
|
||||
* @return Size in pixels.
|
||||
*/
|
||||
public static float dpToPixels(@NonNull final Context context, final float sizeInDp) {
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, sizeInDp, getDisplayMetrics(context));
|
||||
}
|
||||
|
||||
private OfMetrics() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities methods related to activities and it'sintents.
|
||||
*/
|
||||
public static class OfActivities {
|
||||
|
||||
/**
|
||||
* Returns action bar (on top like toolbar or appbar) common height (56dp).
|
||||
*
|
||||
* @param activity Activity of action bar;
|
||||
* @return Height of action bar.
|
||||
*/
|
||||
public static int getActionBarHeight(@NonNull final Activity activity) {
|
||||
return (int) OfMetrics.dpToPixels(activity, 56);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns status bar (on top where system info is) common height.
|
||||
*
|
||||
* @param activity Activity of status bar;
|
||||
* @return Height of status bar.
|
||||
*/
|
||||
public static int getStatusBarHeight(@NonNull final Activity activity) {
|
||||
final int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
|
||||
return resourceId > 0 ? activity.getResources().getDimensionPixelSize(resourceId) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns navigation bar (on bottom where system buttons are) common height.
|
||||
* Be aware that some devices have no software keyboard (check it by {@link #hasSoftKeys(Activity)}) but this method will return you
|
||||
* size like they are showed.
|
||||
*
|
||||
* @param activity Activity of navigation bar;
|
||||
* @return Height of navigation bar.
|
||||
*/
|
||||
public static int getNavigationBarHeight(@NonNull final Activity activity) {
|
||||
if (hasSoftKeys(activity)) {
|
||||
final int resourceId = activity.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
|
||||
return resourceId > 0 ? activity.getResources().getDimensionPixelSize(resourceId) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if device has software keyboard at navigation bar or not.
|
||||
*
|
||||
* @param activity Activity of navigation bar;
|
||||
* @return True if software keyboard is showing at navigation bar.
|
||||
*/
|
||||
//http://stackoverflow.com/questions/14853039/how-to-tell-whether-an-android-device-has-hard-keys/14871974#14871974
|
||||
public static boolean hasSoftKeys(@NonNull final Activity activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
final Display display = activity.getWindowManager().getDefaultDisplay();
|
||||
|
||||
final DisplayMetrics realDisplayMetrics = new DisplayMetrics();
|
||||
display.getRealMetrics(realDisplayMetrics);
|
||||
|
||||
final DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
display.getMetrics(displayMetrics);
|
||||
|
||||
return (realDisplayMetrics.widthPixels - displayMetrics.widthPixels) > 0
|
||||
|| (realDisplayMetrics.heightPixels - displayMetrics.heightPixels) > 0;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
final boolean hasMenuKey = ViewConfiguration.get(activity).hasPermanentMenuKey();
|
||||
final boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
|
||||
return !hasMenuKey && !hasBackKey;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if {@link Intent} could be handled by system.
|
||||
*
|
||||
* @param context {@link Context} of application;
|
||||
* @param intent {@link Intent} to be handled;
|
||||
* @return True if there are handlers for {@link Intent} (e.g. browser could handle URI intent).
|
||||
*/
|
||||
public static boolean isIntentAbleToHandle(@NonNull final Context context, @NonNull final Intent intent) {
|
||||
return !context.getPackageManager().queryIntentActivities(intent, 0).isEmpty();
|
||||
}
|
||||
|
||||
private OfActivities() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities methods related to views.
|
||||
*/
|
||||
public static class OfViews {
|
||||
|
||||
private static final int GENERATED_ID_THRESHOLD = 0x00FFFFFF;
|
||||
private static final AtomicInteger NEXT_GENERATED_ID = new AtomicInteger(1);
|
||||
|
||||
/**
|
||||
* Generates unique ID for view. See android {@link View#generateViewId()}.
|
||||
*
|
||||
* @return Unique ID.
|
||||
*/
|
||||
@IdRes
|
||||
public static int generateViewId() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
return View.generateViewId();
|
||||
}
|
||||
int result = 0;
|
||||
boolean isGenerated = false;
|
||||
while (!isGenerated) {
|
||||
result = NEXT_GENERATED_ID.get();
|
||||
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
|
||||
int newValue = result + 1;
|
||||
if (newValue > GENERATED_ID_THRESHOLD) {
|
||||
newValue = 1; // Roll over to 1, not 0.
|
||||
}
|
||||
if (NEXT_GENERATED_ID.compareAndSet(result, newValue)) {
|
||||
isGenerated = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string representation of {@link View}'s ID.
|
||||
*
|
||||
* @param view {@link View} to get ID from;
|
||||
* @return Readable ID.
|
||||
*/
|
||||
@NonNull
|
||||
public static String getViewIdString(@NonNull final View view) {
|
||||
try {
|
||||
return view.getResources().getResourceName(view.getId());
|
||||
} catch (final Resources.NotFoundException exception) {
|
||||
return String.valueOf(view.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private OfViews() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.AppCompatEditText;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.touchin.roboswag.components.R;
|
||||
import ru.touchin.roboswag.components.utils.Typefaces;
|
||||
import ru.touchin.roboswag.components.views.internal.AttributesCheckUtils;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 18/07/2014.
|
||||
* TextView that supports fonts from Typefaces class
|
||||
*/
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 18/07/2014.
|
||||
* EditText that supports custom typeface and forces developer to specify if this view multiline or not.
|
||||
* Also in debug mode it has common checks for popular bugs.
|
||||
*/
|
||||
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||
//ConstructorCallsOverridableMethod: it's ok as we need to setTypeface
|
||||
public class TypefacedEditText extends AppCompatEditText {
|
||||
|
||||
private static boolean inDebugMode;
|
||||
|
||||
/**
|
||||
* Enables debugging features like checking attributes on inflation.
|
||||
*/
|
||||
public static void setInDebugMode() {
|
||||
inDebugMode = true;
|
||||
}
|
||||
|
||||
private boolean multiline;
|
||||
|
||||
@Nullable
|
||||
private OnTextChangedListener onTextChangedListener;
|
||||
|
||||
public TypefacedEditText(@NonNull final Context context) {
|
||||
super(context);
|
||||
initialize(context, null);
|
||||
}
|
||||
|
||||
public TypefacedEditText(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs);
|
||||
}
|
||||
|
||||
public TypefacedEditText(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs);
|
||||
}
|
||||
|
||||
private void initialize(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||
super.setIncludeFontPadding(false);
|
||||
initializeTextChangedListener();
|
||||
if (attrs != null) {
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedEditText);
|
||||
setMultiline(typedArray.getBoolean(R.styleable.TypefacedEditText_isMultiline, false));
|
||||
if (!isInEditMode()) {
|
||||
setTypeface(Typefaces.getFromAttributes(context, attrs, R.styleable.TypefacedEditText, R.styleable.TypefacedEditText_customTypeface));
|
||||
}
|
||||
typedArray.recycle();
|
||||
if (inDebugMode) {
|
||||
checkAttributes(context, attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs) {
|
||||
final List<String> errors = new ArrayList<>();
|
||||
Boolean multiline = null;
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedEditText);
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedEditText_customTypeface, true,
|
||||
"customTypeface required parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedEditText_isMultiline, true,
|
||||
"isMultiline required parameter");
|
||||
if (typedArray.hasValue(R.styleable.TypefacedEditText_isMultiline)) {
|
||||
multiline = typedArray.getBoolean(R.styleable.TypefacedEditText_isMultiline, false);
|
||||
}
|
||||
typedArray.recycle();
|
||||
|
||||
try {
|
||||
final Class androidRes = Class.forName("com.android.internal.R$styleable");
|
||||
|
||||
typedArray = context.obtainStyledAttributes(attrs, AttributesCheckUtils.getField(androidRes, "TextView"));
|
||||
AttributesCheckUtils.checkRegularTextViewAttributes(typedArray, androidRes, errors, "isMultiline");
|
||||
checkEditTextSpecificAttributes(typedArray, androidRes, errors);
|
||||
if (multiline != null) {
|
||||
checkMultilineAttributes(typedArray, androidRes, errors, multiline);
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
Lc.assertion(exception);
|
||||
}
|
||||
AttributesCheckUtils.handleErrors(this, errors);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
private void checkEditTextSpecificAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes,
|
||||
@NonNull final List<String> errors)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_typeface"), false,
|
||||
"remove typeface and use customTypeface");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_textStyle"), false,
|
||||
"remove textStyle and use customTypeface");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_fontFamily"), false,
|
||||
"remove fontFamily and use customTypeface");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_singleLine"), false,
|
||||
"remove singleLine and use isMultiline");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_includeFontPadding"), false,
|
||||
"includeFontPadding forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_ellipsize"), false,
|
||||
"ellipsize forbid parameter");
|
||||
|
||||
if (typedArray.hasValue(AttributesCheckUtils.getField(androidRes, "TextView_hint"))) {
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_textColorHint"), true,
|
||||
"textColorHint required parameter if hint is not null");
|
||||
}
|
||||
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_textSize"), true,
|
||||
"textSize required parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_inputType"), true,
|
||||
"inputType required parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_imeOptions"), true,
|
||||
"imeOptions required parameter");
|
||||
}
|
||||
|
||||
private void checkMultilineAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes,
|
||||
@NonNull final List<String> errors, final boolean multiline)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
if (multiline) {
|
||||
if (typedArray.getInt(AttributesCheckUtils.getField(androidRes, "TextView_lines"), -1) == 1) {
|
||||
errors.add("lines should be more than 1 if isMultiline is true");
|
||||
}
|
||||
if (typedArray.getInt(AttributesCheckUtils.getField(androidRes, "TextView_maxLines"), -1) == 1) {
|
||||
errors.add("maxLines should be more than 1 if isMultiline is true");
|
||||
}
|
||||
if (!typedArray.hasValue(AttributesCheckUtils.getField(androidRes, "TextView_maxLines"))
|
||||
&& !typedArray.hasValue(AttributesCheckUtils.getField(androidRes, "TextView_maxLength"))) {
|
||||
errors.add("specify maxLines or maxLength if isMultiline is true");
|
||||
}
|
||||
} else {
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_lines"), false,
|
||||
"remove lines and use isMultiline");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_maxLines"), false,
|
||||
"maxLines remove and use isMultiline");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_minLines"), false,
|
||||
"minLines remove and use isMultiline");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_maxLength"), true,
|
||||
"maxLength required parameter if isMultiline is false");
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeTextChangedListener() {
|
||||
addTextChangedListener(new TextWatcher() {
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(final CharSequence oldText, final int start, final int count, final int after) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence inputText, final int start, final int before, final int count) {
|
||||
if (onTextChangedListener != null) {
|
||||
onTextChangedListener.onTextChanged(inputText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable editable) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if view supports multiline text alignment.
|
||||
*
|
||||
* @param multiline True if multiline supported.
|
||||
*/
|
||||
public void setMultiline(final boolean multiline) {
|
||||
this.multiline = multiline;
|
||||
if (multiline) {
|
||||
super.setSingleLine(false);
|
||||
} else {
|
||||
super.setSingleLine(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleLine(final boolean singleLine) {
|
||||
setMultiline(!singleLine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleLine() {
|
||||
setMultiline(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLines(final int lines) {
|
||||
if (multiline && lines == 1) {
|
||||
throw new IllegalStateException("lines = 1 is illegal if multiline is set to true");
|
||||
}
|
||||
super.setLines(lines);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxLines(final int maxLines) {
|
||||
if (!multiline && maxLines > 1) {
|
||||
throw new IllegalStateException("maxLines > 1 is illegal if multiline is set to false");
|
||||
}
|
||||
super.setMaxLines(maxLines);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinLines(final int minLines) {
|
||||
if (!multiline && minLines > 1) {
|
||||
throw new IllegalStateException("minLines > 1 is illegal if multiline is set to false");
|
||||
}
|
||||
super.setMinLines(minLines);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setIncludeFontPadding(final boolean includeFontPadding) {
|
||||
throw new IllegalStateException("Do not specify font padding as it is hard to make pixel-perfect design with such option");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEllipsize(final TextUtils.TruncateAt ellipsis) {
|
||||
throw new IllegalStateException("Do not specify ellipsize for EditText");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets typeface from 'assets/fonts' folder by name.
|
||||
*
|
||||
* @param name Full name of typeface (without extension, e.g. 'Roboto-Regular').
|
||||
*/
|
||||
public void setTypeface(@NonNull final String name) {
|
||||
setTypeface(Typefaces.getByName(getContext(), name));
|
||||
}
|
||||
|
||||
public void setOnTextChangedListener(@Nullable final OnTextChangedListener onTextChangedListener) {
|
||||
this.onTextChangedListener = onTextChangedListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified variant of {@link TextWatcher}.
|
||||
*/
|
||||
public interface OnTextChangedListener {
|
||||
|
||||
/**
|
||||
* Calls when text have changed.
|
||||
*
|
||||
* @param text New text.
|
||||
*/
|
||||
void onTextChanged(@NonNull CharSequence text);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
/*
|
||||
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.views;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.touchin.roboswag.components.R;
|
||||
import ru.touchin.roboswag.components.utils.Typefaces;
|
||||
import ru.touchin.roboswag.components.utils.UiUtils;
|
||||
import ru.touchin.roboswag.components.views.internal.AttributesCheckUtils;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 18/07/2014.
|
||||
* TextView that supports custom typeface and forces developer to specify {@link LineStrategy}.
|
||||
* Also in debug mode it has common checks for popular bugs.
|
||||
*/
|
||||
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
|
||||
//ConstructorCallsOverridableMethod: it's ok as we need to setTypeface
|
||||
public class TypefacedTextView extends AppCompatTextView {
|
||||
|
||||
private static final int UNSPECIFIED_MEASURE_SPEC = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
||||
private static final int START_SCALABLE_DIFFERENCE = 4;
|
||||
|
||||
private static boolean inDebugMode;
|
||||
|
||||
/**
|
||||
* Enables debugging features like checking attributes on inflation.
|
||||
*/
|
||||
public static void setInDebugMode() {
|
||||
inDebugMode = true;
|
||||
}
|
||||
|
||||
//could be null on construction
|
||||
private LineStrategy lineStrategy = LineStrategy.SINGLE_LINE_ELLIPSIZE;
|
||||
|
||||
public TypefacedTextView(@NonNull final Context context) {
|
||||
super(context);
|
||||
initialize(context, null);
|
||||
}
|
||||
|
||||
public TypefacedTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context, attrs);
|
||||
}
|
||||
|
||||
public TypefacedTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(context, attrs);
|
||||
}
|
||||
|
||||
private void initialize(@NonNull final Context context, @Nullable final AttributeSet attrs) {
|
||||
super.setIncludeFontPadding(false);
|
||||
if (attrs != null) {
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedTextView);
|
||||
setLineStrategy(LineStrategy.byResIndex(typedArray.getInt(R.styleable.TypefacedTextView_lineStrategy,
|
||||
LineStrategy.MULTILINE_ELLIPSIZE.ordinal())));
|
||||
if (!isInEditMode()) {
|
||||
setTypeface(Typefaces.getFromAttributes(context, attrs, R.styleable.TypefacedTextView, R.styleable.TypefacedTextView_customTypeface));
|
||||
}
|
||||
typedArray.recycle();
|
||||
if (inDebugMode) {
|
||||
checkAttributes(context, attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs) {
|
||||
final List<String> errors = new ArrayList<>();
|
||||
LineStrategy lineStrategy = null;
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedTextView);
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedTextView_customTypeface, true,
|
||||
"customTypeface required parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedTextView_lineStrategy, true,
|
||||
"lineStrategy required parameter");
|
||||
if (typedArray.hasValue(R.styleable.TypefacedTextView_lineStrategy)) {
|
||||
lineStrategy = LineStrategy.byResIndex(typedArray.getInt(R.styleable.TypefacedTextView_lineStrategy, -1));
|
||||
}
|
||||
typedArray.recycle();
|
||||
|
||||
try {
|
||||
final Class androidRes = Class.forName("com.android.internal.R$styleable");
|
||||
|
||||
typedArray = context.obtainStyledAttributes(attrs, AttributesCheckUtils.getField(androidRes, "TextView"));
|
||||
AttributesCheckUtils.checkRegularTextViewAttributes(typedArray, androidRes, errors, "lineStrategy");
|
||||
checkTextViewSpecificAttributes(typedArray, androidRes, errors);
|
||||
|
||||
if (lineStrategy != null) {
|
||||
checkLineStrategyAttributes(typedArray, androidRes, errors, lineStrategy);
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
Lc.assertion(exception);
|
||||
}
|
||||
AttributesCheckUtils.handleErrors(this, errors);
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
private void checkTextViewSpecificAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes,
|
||||
@NonNull final List<String> errors)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_phoneNumber"), false,
|
||||
"phoneNumber forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_password"), false,
|
||||
"password forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_numeric"), false,
|
||||
"numeric forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_inputType"), false,
|
||||
"inputType forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_imeOptions"), false,
|
||||
"imeOptions forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_imeActionId"), false,
|
||||
"imeActionId forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_imeActionLabel"), false,
|
||||
"imeActionLabel forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_hint"), false,
|
||||
"hint forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_editable"), false,
|
||||
"editable forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_digits"), false,
|
||||
"digits forbid parameter");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_cursorVisible"), false,
|
||||
"cursorVisible forbid parameter");
|
||||
}
|
||||
|
||||
private void checkLineStrategyAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes,
|
||||
@NonNull final List<String> errors, @NonNull final LineStrategy lineStrategy)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
if (!lineStrategy.scalable) {
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_textSize"), true,
|
||||
"textSize required parameter");
|
||||
}
|
||||
if (lineStrategy.multiline) {
|
||||
if (typedArray.getInt(AttributesCheckUtils.getField(androidRes, "TextView_lines"), -1) == 1) {
|
||||
errors.add("lines should be more than 1 if lineStrategy is true");
|
||||
}
|
||||
if (typedArray.getInt(AttributesCheckUtils.getField(androidRes, "TextView_maxLines"), -1) == 1) {
|
||||
errors.add("maxLines should be more than 1 if lineStrategy is true");
|
||||
}
|
||||
} else {
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_lines"), false,
|
||||
"remove lines and use lineStrategy");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_maxLines"), false,
|
||||
"remove maxLines and use lineStrategy");
|
||||
AttributesCheckUtils.checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_minLines"), false,
|
||||
"remove minLines and use lineStrategy");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets behavior of text if there is no space for it in one line.
|
||||
*
|
||||
* @param lineStrategy Specific {@link LineStrategy}.
|
||||
*/
|
||||
public void setLineStrategy(@NonNull final LineStrategy lineStrategy) {
|
||||
this.lineStrategy = lineStrategy;
|
||||
super.setSingleLine(!lineStrategy.multiline);
|
||||
if (lineStrategy.scalable) {
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns behavior of text if there is no space for it in one line.
|
||||
*
|
||||
* @return Specific {@link LineStrategy}.
|
||||
*/
|
||||
@NonNull
|
||||
public LineStrategy getLineStrategy() {
|
||||
return lineStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleLine() {
|
||||
throw new IllegalStateException("Do not specify setSingleLine use setLineStrategy instead");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleLine(final boolean singleLine) {
|
||||
throw new IllegalStateException("Do not specify setSingleLine use setLineStrategy instead");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLines(final int lines) {
|
||||
if (lineStrategy != null && lineStrategy.multiline && lines == 1) {
|
||||
throw new IllegalStateException("lines = 1 is illegal if lineStrategy is multiline");
|
||||
}
|
||||
super.setLines(lines);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxLines(final int maxLines) {
|
||||
if (lineStrategy != null && !lineStrategy.multiline && maxLines > 1) {
|
||||
throw new IllegalStateException("maxLines > 1 is illegal if lineStrategy is single line");
|
||||
}
|
||||
super.setMaxLines(maxLines);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinLines(final int minLines) {
|
||||
if (lineStrategy != null && !lineStrategy.multiline && minLines > 1) {
|
||||
throw new IllegalStateException("minLines > 1 is illegal if lineStrategy is single line");
|
||||
}
|
||||
super.setMinLines(minLines);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setIncludeFontPadding(final boolean includeFontPadding) {
|
||||
throw new IllegalStateException("Do not specify font padding as it is hard to make pixel-perfect design with such option");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEllipsize(final TextUtils.TruncateAt ellipsize) {
|
||||
throw new IllegalStateException("Do not specify ellipsize use setLineStrategy instead");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets typeface from 'assets/fonts' folder by name.
|
||||
*
|
||||
* @param name Full name of typeface (without extension, e.g. 'Roboto-Regular').
|
||||
*/
|
||||
public void setTypeface(@NonNull final String name) {
|
||||
setTypeface(Typefaces.getByName(getContext(), name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setText(final CharSequence text, final BufferType type) {
|
||||
super.setText(text, type);
|
||||
if (lineStrategy != null && lineStrategy.scalable) {
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextSize(final float size) {
|
||||
if (lineStrategy != null && lineStrategy.scalable) {
|
||||
throw new IllegalStateException("textSize call is illegal if lineStrategy is scalable");
|
||||
}
|
||||
super.setTextSize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextSize(final int unit, final float size) {
|
||||
if (lineStrategy != null && lineStrategy.scalable) {
|
||||
throw new IllegalStateException("textSize call is illegal if lineStrategy is scalable");
|
||||
}
|
||||
super.setTextSize(unit, size);
|
||||
}
|
||||
|
||||
@SuppressLint("WrongCall")
|
||||
//WrongCall: actually this method is always calling from onMeasure
|
||||
private void computeScalableTextSize(final int maxWidth, final int maxHeight) {
|
||||
final int minDifference = (int) UiUtils.OfMetrics.dpToPixels(getContext(), 1);
|
||||
int difference = (int) UiUtils.OfMetrics.dpToPixels(getContext(), START_SCALABLE_DIFFERENCE);
|
||||
ScaleAction scaleAction = ScaleAction.DO_NOTHING;
|
||||
ScaleAction previousScaleAction = ScaleAction.DO_NOTHING;
|
||||
do {
|
||||
switch (scaleAction) {
|
||||
case SCALE_DOWN:
|
||||
if (difference > minDifference) {
|
||||
difference -= minDifference;
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() - difference));
|
||||
} else {
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() - minDifference));
|
||||
if (previousScaleAction == ScaleAction.SCALE_UP) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SCALE_UP:
|
||||
if (difference > minDifference) {
|
||||
difference -= minDifference;
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() + difference));
|
||||
} else {
|
||||
if (previousScaleAction == ScaleAction.SCALE_DOWN) {
|
||||
return;
|
||||
}
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() + minDifference));
|
||||
}
|
||||
break;
|
||||
case DO_NOTHING:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
super.onMeasure(UNSPECIFIED_MEASURE_SPEC, UNSPECIFIED_MEASURE_SPEC);
|
||||
previousScaleAction = scaleAction;
|
||||
scaleAction = computeScaleAction(maxWidth, maxHeight);
|
||||
} while (scaleAction != ScaleAction.DO_NOTHING);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ScaleAction computeScaleAction(final int maxWidth, final int maxHeight) {
|
||||
ScaleAction result = ScaleAction.DO_NOTHING;
|
||||
if (maxWidth < getMeasuredWidth()) {
|
||||
result = ScaleAction.SCALE_DOWN;
|
||||
} else if (maxWidth > getMeasuredWidth()) {
|
||||
result = ScaleAction.SCALE_UP;
|
||||
}
|
||||
|
||||
if (maxHeight < getMeasuredHeight()) {
|
||||
result = ScaleAction.SCALE_DOWN;
|
||||
} else if (maxHeight > getMeasuredHeight() && result != ScaleAction.SCALE_DOWN) {
|
||||
result = ScaleAction.SCALE_UP;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
final int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
final int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
if (lineStrategy == null || !lineStrategy.scalable || maxWidth <= 0 || maxHeight <= 0 || TextUtils.isEmpty(getText())) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
computeScalableTextSize(maxWidth, maxHeight);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
private enum ScaleAction {
|
||||
SCALE_DOWN,
|
||||
SCALE_UP,
|
||||
DO_NOTHING
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific behavior, mostly based on combination of {@link #getEllipsize()} and {@link #getMaxLines()} to specify how view should show text
|
||||
* if there is no space for it on one line.
|
||||
*/
|
||||
public enum LineStrategy {
|
||||
|
||||
/**
|
||||
* Not more than one line and ellipsize text with dots at end.
|
||||
*/
|
||||
SINGLE_LINE_ELLIPSIZE(false, false),
|
||||
/**
|
||||
* Not more than one line and ellipsize text with marquee at end.
|
||||
*/
|
||||
SINGLE_LINE_MARQUEE(false, false),
|
||||
/**
|
||||
* Not more than one line and scale text to maximum possible size.
|
||||
*/
|
||||
SINGLE_LINE_AUTO_SCALE(false, true),
|
||||
/**
|
||||
* More than one line and ellipsize text with dots at end.
|
||||
*/
|
||||
MULTILINE_ELLIPSIZE(true, false),
|
||||
/**
|
||||
* More than one line and ellipsize text with marquee at end.
|
||||
*/
|
||||
MULTILINE_MARQUEE(true, false);
|
||||
|
||||
@NonNull
|
||||
public static LineStrategy byResIndex(final int resIndex) {
|
||||
if (resIndex < 0 || resIndex >= values().length) {
|
||||
Lc.assertion("Unexpected resIndex " + resIndex);
|
||||
return MULTILINE_ELLIPSIZE;
|
||||
}
|
||||
return values()[resIndex];
|
||||
}
|
||||
|
||||
private final boolean multiline;
|
||||
private final boolean scalable;
|
||||
|
||||
LineStrategy(final boolean multiline, final boolean scalable) {
|
||||
this.multiline = multiline;
|
||||
this.scalable = scalable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
|
||||
*
|
||||
* This file is part of RoboSwag library.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package ru.touchin.roboswag.components.views.internal;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StyleableRes;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collection;
|
||||
|
||||
import ru.touchin.roboswag.components.utils.UiUtils;
|
||||
import ru.touchin.roboswag.core.log.Lc;
|
||||
|
||||
/**
|
||||
* Created by Gavriil Sitnikov on 13/06/2016.
|
||||
* Bunch of inner helper library methods to validate attributes of custom views.
|
||||
*/
|
||||
public final class AttributesCheckUtils {
|
||||
|
||||
/**
|
||||
* Gets static field of class.
|
||||
*
|
||||
* @param resourcesClass Class to get field from;
|
||||
* @param fieldName name of field;
|
||||
* @param <T> Type of object that is stored in field;
|
||||
* @return Field value;
|
||||
* @throws NoSuchFieldException Throws on reflection call;
|
||||
* @throws IllegalAccessException Throws on reflection call.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getField(@NonNull final Class resourcesClass, @NonNull final String fieldName)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
final Field field = resourcesClass.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return (T) field.get(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if attribute is in array or not and collecterror if attribute missed.
|
||||
*
|
||||
* @param typedArray Array of attributes;
|
||||
* @param errors Errors to collect into;
|
||||
* @param resourceId Id of attribute;
|
||||
* @param required Is parameter have to be in array OR it have not to be in;
|
||||
* @param description Description of error.
|
||||
*/
|
||||
public static void checkAttribute(@NonNull final TypedArray typedArray,
|
||||
@NonNull final Collection<String> errors,
|
||||
@StyleableRes final int resourceId,
|
||||
final boolean required,
|
||||
@NonNull final String description) {
|
||||
if ((required && typedArray.hasValue(resourceId))
|
||||
|| (!required && !typedArray.hasValue(resourceId))) {
|
||||
return;
|
||||
}
|
||||
errors.add(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects regular {@link android.widget.TextView} errors.
|
||||
*
|
||||
* @param typedArray Array of attributes;
|
||||
* @param androidRes Class of styleable attributes;
|
||||
* @param errors Errors to collect into;
|
||||
* @param lineStrategyParameterName name of line strategy parameter;
|
||||
* @throws NoSuchFieldException Throws during getting attribute values through reflection;
|
||||
* @throws IllegalAccessException Throws during getting attribute values through reflection.
|
||||
*/
|
||||
public static void checkRegularTextViewAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes,
|
||||
@NonNull final Collection<String> errors, @NonNull final String lineStrategyParameterName)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
checkAttribute(typedArray, errors, getField(androidRes, "TextView_typeface"), false, "remove typeface and use customTypeface");
|
||||
checkAttribute(typedArray, errors, getField(androidRes, "TextView_textStyle"), false, "remove textStyle and use customTypeface");
|
||||
checkAttribute(typedArray, errors, getField(androidRes, "TextView_fontFamily"), false, "remove fontFamily and use customTypeface");
|
||||
checkAttribute(typedArray, errors, getField(androidRes, "TextView_includeFontPadding"), false, "includeFontPadding forbid parameter");
|
||||
checkAttribute(typedArray, errors, getField(androidRes, "TextView_singleLine"), false,
|
||||
"remove singleLine and use " + lineStrategyParameterName);
|
||||
checkAttribute(typedArray, errors, getField(androidRes, "TextView_ellipsize"), false,
|
||||
"remove ellipsize and use " + lineStrategyParameterName);
|
||||
checkAttribute(typedArray, errors, AttributesCheckUtils.getField(androidRes, "TextView_textColor"), true, "textColor required parameter");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner helper library method to merge errors in string and assert it.
|
||||
*
|
||||
* @param view View with errors;
|
||||
* @param errors Errors of view.
|
||||
*/
|
||||
public static void handleErrors(@NonNull final View view, @NonNull final Collection<String> errors) {
|
||||
if (!errors.isEmpty()) {
|
||||
Lc.assertion("Errors for view [" + UiUtils.OfViews.getViewIdString(view) + "]:\n" + TextUtils.join("\n", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private AttributesCheckUtils() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<attr name="customTypeface" format="string"/>
|
||||
|
||||
<declare-styleable name="TypefacedTextView">
|
||||
<attr name="customTypeface"/>
|
||||
<attr name="lineStrategy" format="enum">
|
||||
<enum name="singleLineEllipsize" value="0"/>
|
||||
<enum name="singleLineMarquee" value="1"/>
|
||||
<enum name="singleLineAutoScale" value="2"/>
|
||||
<enum name="multilineEllipsize" value="3"/>
|
||||
<enum name="multilineMarquee" value="4"/>
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="TypefacedEditText">
|
||||
<attr name="customTypeface"/>
|
||||
<attr name="isMultiline" format="boolean"/>
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue