Typefaced views logic in release candidate state

This commit is contained in:
Gavriil Sitnikov 2016-06-14 00:40:10 +03:00
parent 2bd7e1e967
commit 45d39b2df8
7 changed files with 1277 additions and 6 deletions

View File

@ -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();

View File

@ -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() {
}
}

View File

@ -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() {
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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() {
}
}

View File

@ -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>