diff --git a/app/build.gradle b/app/build.gradle index 7aa0dc0f25..bb72725052 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -169,6 +169,7 @@ allprojects { dependencies { wearApp project(':wear') + implementation project(':iconify') implementation project(':shared') implementation project(':core') implementation project(':automation') diff --git a/combo/build.gradle b/combo/build.gradle index a3faa431e6..e20cc0fc66 100644 --- a/combo/build.gradle +++ b/combo/build.gradle @@ -15,4 +15,5 @@ android { dependencies { implementation project(':core') implementation project(':shared') + implementation project(':iconify') } \ No newline at end of file diff --git a/core/core_dependencies.gradle b/core/core_dependencies.gradle index 5b4df7cbe5..320e025b5c 100644 --- a/core/core_dependencies.gradle +++ b/core/core_dependencies.gradle @@ -69,8 +69,6 @@ dependencies { api "com.squareup.retrofit2:adapter-rxjava3:$retrofit2_version" api "com.squareup.retrofit2:converter-gson:$retrofit2_version" - api "com.joanzapata.iconify:android-iconify-fontawesome:2.2.2" - api "androidx.fragment:fragment-ktx:$fragmentktx_version" api "androidx.constraintlayout:constraintlayout:$constraintlayout_version" api "androidx.navigation:navigation-runtime-ktx:$nav_version" diff --git a/dana/build.gradle b/dana/build.gradle index f03521d269..5a166b3ab2 100644 --- a/dana/build.gradle +++ b/dana/build.gradle @@ -25,6 +25,7 @@ android { dependencies { implementation project(':core') implementation project(':shared') + implementation project(':iconify') api "androidx.room:room-ktx:$room_version" api "androidx.room:room-runtime:$room_version" diff --git a/diaconn/build.gradle b/diaconn/build.gradle index 84034c07ae..64ac7fee1f 100644 --- a/diaconn/build.gradle +++ b/diaconn/build.gradle @@ -25,6 +25,7 @@ android { dependencies { implementation project(':core') implementation project(':shared') + implementation project(':iconify') api "androidx.room:room-ktx:$room_version" api "androidx.room:room-runtime:$room_version" diff --git a/iconify/.gitignore b/iconify/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/iconify/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/iconify/build.gradle b/iconify/build.gradle new file mode 100644 index 0000000000..3166680705 --- /dev/null +++ b/iconify/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-allopen' +apply plugin: 'com.hiya.jacoco-android' +apply plugin: 'kotlinx-serialization' + +apply from: "${project.rootDir}/core/android_dependencies.gradle" +apply from: "${project.rootDir}/core/android_module_dependencies.gradle" +apply from: "${project.rootDir}/core/test_dependencies.gradle" +apply from: "${project.rootDir}/core/jacoco_global.gradle" + +android { + + namespace 'com.joanzapata.iconify' +} + +dependencies { + api "androidx.core:core-ktx:$core_version" +} \ No newline at end of file diff --git a/iconify/consumer-rules.pro b/iconify/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/iconify/proguard-rules.pro b/iconify/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/iconify/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/iconify/src/main/AndroidManifest.xml b/iconify/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a5918e68ab --- /dev/null +++ b/iconify/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/iconify/src/main/assets/iconify/android-iconify-fontawesome.ttf b/iconify/src/main/assets/iconify/android-iconify-fontawesome.ttf new file mode 100644 index 0000000000..26dea7951a Binary files /dev/null and b/iconify/src/main/assets/iconify/android-iconify-fontawesome.ttf differ diff --git a/iconify/src/main/java/com/joanzapata/iconify/Icon.java b/iconify/src/main/java/com/joanzapata/iconify/Icon.java new file mode 100644 index 0000000000..11f6921289 --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/Icon.java @@ -0,0 +1,14 @@ +package com.joanzapata.iconify; + +/** + * Icon represents one icon in an icon font. + */ +public interface Icon { + + /** The key of icon, for example 'fa-ok' */ + String key(); + + /** The character matching the key in the font, for example '\u4354' */ + char character(); + +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/IconDrawable.java b/iconify/src/main/java/com/joanzapata/iconify/IconDrawable.java new file mode 100644 index 0000000000..ea8783672e --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/IconDrawable.java @@ -0,0 +1,229 @@ +package com.joanzapata.iconify; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.TypedValue; +import com.joanzapata.iconify.internal.IconFontDescriptorWrapper; + +import static android.util.TypedValue.COMPLEX_UNIT_DIP; + +/** + * Embed an icon into a Drawable that can be used as TextView icons, or ActionBar icons. + *
+ *     new IconDrawable(context, IconValue.icon_star)
+ *           .colorRes(R.color.white)
+ *           .actionBarSize();
+ * 
+ * If you don't set the size of the drawable, it will use the size + * that is given to him. Note that in an ActionBar, if you don't + * set the size explicitly it uses 0, so please use actionBarSize(). + */ +public class IconDrawable extends Drawable { + + public static final int ANDROID_ACTIONBAR_ICON_SIZE_DP = 24; + + private Context context; + + private Icon icon; + + private TextPaint paint; + + private int size = -1; + + private int alpha = 255; + + /** + * Create an IconDrawable. + * @param context Your activity or application context. + * @param iconKey The icon key you want this drawable to display. + * @throws IllegalArgumentException if the key doesn't match any icon. + */ + public IconDrawable(Context context, String iconKey) { + Icon icon = Iconify.findIconForKey(iconKey); + if (icon == null) { + throw new IllegalArgumentException("No icon with that key \"" + iconKey + "\"."); + } + init(context, icon); + } + + /** + * Create an IconDrawable. + * @param context Your activity or application context. + * @param icon The icon you want this drawable to display. + */ + public IconDrawable(Context context, Icon icon) { + init(context, icon); + } + + private void init(Context context, Icon icon) { + this.context = context; + this.icon = icon; + paint = new TextPaint(); + IconFontDescriptorWrapper descriptor = Iconify.findTypefaceOf(icon); + if (descriptor == null) { + throw new IllegalStateException("Unable to find the module associated " + + "with icon " + icon.key() + ", have you registered the module " + + "you are trying to use with Iconify.with(...) in your Application?"); + } + paint.setTypeface(descriptor.getTypeface(context)); + paint.setStyle(Paint.Style.FILL); + paint.setTextAlign(Paint.Align.CENTER); + paint.setUnderlineText(false); + paint.setColor(Color.BLACK); + paint.setAntiAlias(true); + } + + /** + * Set the size of this icon to the standard Android ActionBar. + * @return The current IconDrawable for chaining. + */ + public IconDrawable actionBarSize() { + return sizeDp(ANDROID_ACTIONBAR_ICON_SIZE_DP); + } + + /** + * Set the size of the drawable. + * @param dimenRes The dimension resource. + * @return The current IconDrawable for chaining. + */ + public IconDrawable sizeRes(int dimenRes) { + return sizePx(context.getResources().getDimensionPixelSize(dimenRes)); + } + + /** + * Set the size of the drawable. + * @param size The size in density-independent pixels (dp). + * @return The current IconDrawable for chaining. + */ + public IconDrawable sizeDp(int size) { + return sizePx(convertDpToPx(context, size)); + } + + /** + * Set the size of the drawable. + * @param size The size in pixels (px). + * @return The current IconDrawable for chaining. + */ + public IconDrawable sizePx(int size) { + this.size = size; + setBounds(0, 0, size, size); + invalidateSelf(); + return this; + } + + /** + * Set the color of the drawable. + * @param color The color, usually from android.graphics.Color or 0xFF012345. + * @return The current IconDrawable for chaining. + */ + public IconDrawable color(int color) { + paint.setColor(color); + invalidateSelf(); + return this; + } + + /** + * Set the color of the drawable. + * @param colorRes The color resource, from your R file. + * @return The current IconDrawable for chaining. + */ + public IconDrawable colorRes(int colorRes) { + paint.setColor(context.getColor(colorRes)); + invalidateSelf(); + return this; + } + + /** + * Set the alpha of this drawable. + * @param alpha The alpha, between 0 (transparent) and 255 (opaque). + * @return The current IconDrawable for chaining. + */ + public IconDrawable alpha(int alpha) { + setAlpha(alpha); + invalidateSelf(); + return this; + } + + @Override + public int getIntrinsicHeight() { + return size; + } + + @Override + public int getIntrinsicWidth() { + return size; + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + int height = bounds.height(); + paint.setTextSize(height); + Rect textBounds = new Rect(); + String textValue = String.valueOf(icon.character()); + paint.getTextBounds(textValue, 0, 1, textBounds); + int textHeight = textBounds.height(); + float textBottom = bounds.top + (height - textHeight) / 2f + textHeight - textBounds.bottom; + canvas.drawText(textValue, bounds.exactCenterX(), textBottom, paint); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + public boolean setState(int[] stateSet) { + int oldValue = paint.getAlpha(); + int newValue = isEnabled(stateSet) ? alpha : alpha / 2; + paint.setAlpha(newValue); + return oldValue != newValue; + } + + @Override + public void setAlpha(int alpha) { + this.alpha = alpha; + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + paint.setColorFilter(cf); + } + + @Override + public void clearColorFilter() { + paint.setColorFilter(null); + } + + @SuppressLint("WrongConstant") @Override + public int getOpacity() { + return this.alpha; + } + + /** + * Sets paint style. + * @param style to be applied + */ + public void setStyle(Paint.Style style) { + paint.setStyle(style); + } + + // Util + private boolean isEnabled(int[] stateSet) { + for (int state : stateSet) + if (state == android.R.attr.state_enabled) + return true; + return false; + } + + // Util + private int convertDpToPx(Context context, float dp) { + return (int) TypedValue.applyDimension( + COMPLEX_UNIT_DIP, dp, + context.getResources().getDisplayMetrics()); + } +} \ No newline at end of file diff --git a/iconify/src/main/java/com/joanzapata/iconify/IconFontDescriptor.java b/iconify/src/main/java/com/joanzapata/iconify/IconFontDescriptor.java new file mode 100644 index 0000000000..beb82eb5ff --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/IconFontDescriptor.java @@ -0,0 +1,17 @@ +package com.joanzapata.iconify; + +/** + * An IconFontDescriptor defines a TTF font file + * and is able to map keys with characters in this file. + */ +public interface IconFontDescriptor { + + /** + * The TTF file name. + * @return a name with no slash, present in the assets. + */ + String ttfFileName(); + + Icon[] characters(); + +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/Iconify.java b/iconify/src/main/java/com/joanzapata/iconify/Iconify.java new file mode 100644 index 0000000000..fd5f0a8d23 --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/Iconify.java @@ -0,0 +1,111 @@ +package com.joanzapata.iconify; + +import android.content.Context; +import android.widget.TextView; +import com.joanzapata.iconify.internal.IconFontDescriptorWrapper; +import com.joanzapata.iconify.internal.ParsingUtil; + +import java.util.ArrayList; +import java.util.List; + +public class Iconify { + + /** List of icon font descriptors */ + private static List iconFontDescriptors = new ArrayList(); + + /** + * Add support for a new icon font. + * @param iconFontDescriptor The IconDescriptor holding the ttf file reference and its mappings. + * @return An initializer instance for chain calls. + */ + public static IconifyInitializer with(IconFontDescriptor iconFontDescriptor) { + return new IconifyInitializer(iconFontDescriptor); + } + + /** + * Replace "{}" tags in the given text views with actual icons, requesting the IconFontDescriptors + * one after the others.

+ * This is a one time call. If you call {@link TextView#setText(CharSequence)} after this, + * you'll need to call it again. + * @param textViews The TextView(s) to enhance. + */ + public static void addIcons(TextView... textViews) { + for (TextView textView : textViews) { + if (textView == null) continue; + textView.setText(compute(textView.getContext(), textView.getText(), textView)); + } + } + + private static void addIconFontDescriptor(IconFontDescriptor iconFontDescriptor) { + + // Prevent duplicates + for (IconFontDescriptorWrapper wrapper : iconFontDescriptors) { + if (wrapper.getIconFontDescriptor().ttfFileName() + .equals(iconFontDescriptor.ttfFileName())) { + return; + } + } + + // Add to the list + iconFontDescriptors.add(new IconFontDescriptorWrapper(iconFontDescriptor)); + + } + + public static CharSequence compute(Context context, CharSequence text) { + return compute(context, text, null); + } + + public static CharSequence compute(Context context, CharSequence text, TextView target) { + return ParsingUtil.parse(context, iconFontDescriptors, text, target); + } + + /** + * Allows chain calls on {@link Iconify#with(IconFontDescriptor)}. + */ + public static class IconifyInitializer { + + public IconifyInitializer(IconFontDescriptor iconFontDescriptor) { + Iconify.addIconFontDescriptor(iconFontDescriptor); + } + + /** + * Add support for a new icon font. + * @param iconFontDescriptor The IconDescriptor holding the ttf file reference and its mappings. + * @return An initializer instance for chain calls. + */ + public IconifyInitializer with(IconFontDescriptor iconFontDescriptor) { + Iconify.addIconFontDescriptor(iconFontDescriptor); + return this; + } + } + + /** + * Finds the Typeface to apply for a given icon. + * @param icon The icon for which you need the typeface. + * @return The font descriptor which contains info about the typeface to apply, or null + * if the icon cannot be found. In that case, check that you properly added the modules + * using {@link #with(IconFontDescriptor)}} prior to calling this method. + */ + public static IconFontDescriptorWrapper findTypefaceOf(Icon icon) { + for (IconFontDescriptorWrapper iconFontDescriptor : iconFontDescriptors) { + if (iconFontDescriptor.hasIcon(icon)) { + return iconFontDescriptor; + } + } + return null; + } + + + /** + * Retrieve an icon from a key, + * @return The icon, or null if no icon matches the key. + */ + public static Icon findIconForKey(String iconKey) { + for (int i = 0, iconFontDescriptorsSize = iconFontDescriptors.size(); i < iconFontDescriptorsSize; i++) { + IconFontDescriptorWrapper iconFontDescriptor = iconFontDescriptors.get(i); + Icon icon = iconFontDescriptor.getIcon(iconKey); + if (icon != null) return icon; + } + return null; + } +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/fonts/FontAwesomeIcons.java b/iconify/src/main/java/com/joanzapata/iconify/fonts/FontAwesomeIcons.java new file mode 100644 index 0000000000..16202e243c --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/fonts/FontAwesomeIcons.java @@ -0,0 +1,716 @@ +package com.joanzapata.iconify.fonts; + +import com.joanzapata.iconify.Icon; + +public enum FontAwesomeIcons implements Icon { + fa_glass('\uf000'), + fa_music('\uf001'), + fa_search('\uf002'), + fa_envelope_o('\uf003'), + fa_heart('\uf004'), + fa_star('\uf005'), + fa_star_o('\uf006'), + fa_user('\uf007'), + fa_film('\uf008'), + fa_th_large('\uf009'), + fa_th('\uf00a'), + fa_th_list('\uf00b'), + fa_check('\uf00c'), + fa_remove('\uf00d'), + fa_close('\uf00d'), + fa_times('\uf00d'), + fa_search_plus('\uf00e'), + fa_search_minus('\uf010'), + fa_power_off('\uf011'), + fa_signal('\uf012'), + fa_gear('\uf013'), + fa_cog('\uf013'), + fa_trash_o('\uf014'), + fa_home('\uf015'), + fa_file_o('\uf016'), + fa_clock_o('\uf017'), + fa_road('\uf018'), + fa_download('\uf019'), + fa_arrow_circle_o_down('\uf01a'), + fa_arrow_circle_o_up('\uf01b'), + fa_inbox('\uf01c'), + fa_play_circle_o('\uf01d'), + fa_rotate_right('\uf01e'), + fa_repeat('\uf01e'), + fa_refresh('\uf021'), + fa_list_alt('\uf022'), + fa_lock('\uf023'), + fa_flag('\uf024'), + fa_headphones('\uf025'), + fa_volume_off('\uf026'), + fa_volume_down('\uf027'), + fa_volume_up('\uf028'), + fa_qrcode('\uf029'), + fa_barcode('\uf02a'), + fa_tag('\uf02b'), + fa_tags('\uf02c'), + fa_book('\uf02d'), + fa_bookmark('\uf02e'), + fa_print('\uf02f'), + fa_camera('\uf030'), + fa_font('\uf031'), + fa_bold('\uf032'), + fa_italic('\uf033'), + fa_text_height('\uf034'), + fa_text_width('\uf035'), + fa_align_left('\uf036'), + fa_align_center('\uf037'), + fa_align_right('\uf038'), + fa_align_justify('\uf039'), + fa_list('\uf03a'), + fa_dedent('\uf03b'), + fa_outdent('\uf03b'), + fa_indent('\uf03c'), + fa_video_camera('\uf03d'), + fa_photo('\uf03e'), + fa_image('\uf03e'), + fa_picture_o('\uf03e'), + fa_pencil('\uf040'), + fa_map_marker('\uf041'), + fa_adjust('\uf042'), + fa_tint('\uf043'), + fa_edit('\uf044'), + fa_pencil_square_o('\uf044'), + fa_share_square_o('\uf045'), + fa_check_square_o('\uf046'), + fa_arrows('\uf047'), + fa_step_backward('\uf048'), + fa_fast_backward('\uf049'), + fa_backward('\uf04a'), + fa_play('\uf04b'), + fa_pause('\uf04c'), + fa_stop('\uf04d'), + fa_forward('\uf04e'), + fa_fast_forward('\uf050'), + fa_step_forward('\uf051'), + fa_eject('\uf052'), + fa_chevron_left('\uf053'), + fa_chevron_right('\uf054'), + fa_plus_circle('\uf055'), + fa_minus_circle('\uf056'), + fa_times_circle('\uf057'), + fa_check_circle('\uf058'), + fa_question_circle('\uf059'), + fa_info_circle('\uf05a'), + fa_crosshairs('\uf05b'), + fa_times_circle_o('\uf05c'), + fa_check_circle_o('\uf05d'), + fa_ban('\uf05e'), + fa_arrow_left('\uf060'), + fa_arrow_right('\uf061'), + fa_arrow_up('\uf062'), + fa_arrow_down('\uf063'), + fa_mail_forward('\uf064'), + fa_share('\uf064'), + fa_expand('\uf065'), + fa_compress('\uf066'), + fa_plus('\uf067'), + fa_minus('\uf068'), + fa_asterisk('\uf069'), + fa_exclamation_circle('\uf06a'), + fa_gift('\uf06b'), + fa_leaf('\uf06c'), + fa_fire('\uf06d'), + fa_eye('\uf06e'), + fa_eye_slash('\uf070'), + fa_warning('\uf071'), + fa_exclamation_triangle('\uf071'), + fa_plane('\uf072'), + fa_calendar('\uf073'), + fa_random('\uf074'), + fa_comment('\uf075'), + fa_magnet('\uf076'), + fa_chevron_up('\uf077'), + fa_chevron_down('\uf078'), + fa_retweet('\uf079'), + fa_shopping_cart('\uf07a'), + fa_folder('\uf07b'), + fa_folder_open('\uf07c'), + fa_arrows_v('\uf07d'), + fa_arrows_h('\uf07e'), + fa_bar_chart_o('\uf080'), + fa_bar_chart('\uf080'), + fa_twitter_square('\uf081'), + fa_facebook_square('\uf082'), + fa_camera_retro('\uf083'), + fa_key('\uf084'), + fa_gears('\uf085'), + fa_cogs('\uf085'), + fa_comments('\uf086'), + fa_thumbs_o_up('\uf087'), + fa_thumbs_o_down('\uf088'), + fa_star_half('\uf089'), + fa_heart_o('\uf08a'), + fa_sign_out('\uf08b'), + fa_linkedin_square('\uf08c'), + fa_thumb_tack('\uf08d'), + fa_external_link('\uf08e'), + fa_sign_in('\uf090'), + fa_trophy('\uf091'), + fa_github_square('\uf092'), + fa_upload('\uf093'), + fa_lemon_o('\uf094'), + fa_phone('\uf095'), + fa_square_o('\uf096'), + fa_bookmark_o('\uf097'), + fa_phone_square('\uf098'), + fa_twitter('\uf099'), + fa_facebook_f('\uf09a'), + fa_facebook('\uf09a'), + fa_github('\uf09b'), + fa_unlock('\uf09c'), + fa_credit_card('\uf09d'), + fa_feed('\uf09e'), + fa_rss('\uf09e'), + fa_hdd_o('\uf0a0'), + fa_bullhorn('\uf0a1'), + fa_bell('\uf0f3'), + fa_certificate('\uf0a3'), + fa_hand_o_right('\uf0a4'), + fa_hand_o_left('\uf0a5'), + fa_hand_o_up('\uf0a6'), + fa_hand_o_down('\uf0a7'), + fa_arrow_circle_left('\uf0a8'), + fa_arrow_circle_right('\uf0a9'), + fa_arrow_circle_up('\uf0aa'), + fa_arrow_circle_down('\uf0ab'), + fa_globe('\uf0ac'), + fa_wrench('\uf0ad'), + fa_tasks('\uf0ae'), + fa_filter('\uf0b0'), + fa_briefcase('\uf0b1'), + fa_arrows_alt('\uf0b2'), + fa_group('\uf0c0'), + fa_users('\uf0c0'), + fa_chain('\uf0c1'), + fa_link('\uf0c1'), + fa_cloud('\uf0c2'), + fa_flask('\uf0c3'), + fa_cut('\uf0c4'), + fa_scissors('\uf0c4'), + fa_copy('\uf0c5'), + fa_files_o('\uf0c5'), + fa_paperclip('\uf0c6'), + fa_save('\uf0c7'), + fa_floppy_o('\uf0c7'), + fa_square('\uf0c8'), + fa_navicon('\uf0c9'), + fa_reorder('\uf0c9'), + fa_bars('\uf0c9'), + fa_list_ul('\uf0ca'), + fa_list_ol('\uf0cb'), + fa_strikethrough('\uf0cc'), + fa_underline('\uf0cd'), + fa_table('\uf0ce'), + fa_magic('\uf0d0'), + fa_truck('\uf0d1'), + fa_pinterest('\uf0d2'), + fa_pinterest_square('\uf0d3'), + fa_google_plus_square('\uf0d4'), + fa_google_plus('\uf0d5'), + fa_money('\uf0d6'), + fa_caret_down('\uf0d7'), + fa_caret_up('\uf0d8'), + fa_caret_left('\uf0d9'), + fa_caret_right('\uf0da'), + fa_columns('\uf0db'), + fa_unsorted('\uf0dc'), + fa_sort('\uf0dc'), + fa_sort_down('\uf0dd'), + fa_sort_desc('\uf0dd'), + fa_sort_up('\uf0de'), + fa_sort_asc('\uf0de'), + fa_envelope('\uf0e0'), + fa_linkedin('\uf0e1'), + fa_rotate_left('\uf0e2'), + fa_undo('\uf0e2'), + fa_legal('\uf0e3'), + fa_gavel('\uf0e3'), + fa_dashboard('\uf0e4'), + fa_tachometer('\uf0e4'), + fa_comment_o('\uf0e5'), + fa_comments_o('\uf0e6'), + fa_flash('\uf0e7'), + fa_bolt('\uf0e7'), + fa_sitemap('\uf0e8'), + fa_umbrella('\uf0e9'), + fa_paste('\uf0ea'), + fa_clipboard('\uf0ea'), + fa_lightbulb_o('\uf0eb'), + fa_exchange('\uf0ec'), + fa_cloud_download('\uf0ed'), + fa_cloud_upload('\uf0ee'), + fa_user_md('\uf0f0'), + fa_stethoscope('\uf0f1'), + fa_suitcase('\uf0f2'), + fa_bell_o('\uf0a2'), + fa_coffee('\uf0f4'), + fa_cutlery('\uf0f5'), + fa_file_text_o('\uf0f6'), + fa_building_o('\uf0f7'), + fa_hospital_o('\uf0f8'), + fa_ambulance('\uf0f9'), + fa_medkit('\uf0fa'), + fa_fighter_jet('\uf0fb'), + fa_beer('\uf0fc'), + fa_h_square('\uf0fd'), + fa_plus_square('\uf0fe'), + fa_angle_double_left('\uf100'), + fa_angle_double_right('\uf101'), + fa_angle_double_up('\uf102'), + fa_angle_double_down('\uf103'), + fa_angle_left('\uf104'), + fa_angle_right('\uf105'), + fa_angle_up('\uf106'), + fa_angle_down('\uf107'), + fa_desktop('\uf108'), + fa_laptop('\uf109'), + fa_tablet('\uf10a'), + fa_mobile_phone('\uf10b'), + fa_mobile('\uf10b'), + fa_circle_o('\uf10c'), + fa_quote_left('\uf10d'), + fa_quote_right('\uf10e'), + fa_spinner('\uf110'), + fa_circle('\uf111'), + fa_mail_reply('\uf112'), + fa_reply('\uf112'), + fa_github_alt('\uf113'), + fa_folder_o('\uf114'), + fa_folder_open_o('\uf115'), + fa_smile_o('\uf118'), + fa_frown_o('\uf119'), + fa_meh_o('\uf11a'), + fa_gamepad('\uf11b'), + fa_keyboard_o('\uf11c'), + fa_flag_o('\uf11d'), + fa_flag_checkered('\uf11e'), + fa_terminal('\uf120'), + fa_code('\uf121'), + fa_mail_reply_all('\uf122'), + fa_reply_all('\uf122'), + fa_star_half_empty('\uf123'), + fa_star_half_full('\uf123'), + fa_star_half_o('\uf123'), + fa_location_arrow('\uf124'), + fa_crop('\uf125'), + fa_code_fork('\uf126'), + fa_unlink('\uf127'), + fa_chain_broken('\uf127'), + fa_question('\uf128'), + fa_info('\uf129'), + fa_exclamation('\uf12a'), + fa_superscript('\uf12b'), + fa_subscript('\uf12c'), + fa_eraser('\uf12d'), + fa_puzzle_piece('\uf12e'), + fa_microphone('\uf130'), + fa_microphone_slash('\uf131'), + fa_shield('\uf132'), + fa_calendar_o('\uf133'), + fa_fire_extinguisher('\uf134'), + fa_rocket('\uf135'), + fa_maxcdn('\uf136'), + fa_chevron_circle_left('\uf137'), + fa_chevron_circle_right('\uf138'), + fa_chevron_circle_up('\uf139'), + fa_chevron_circle_down('\uf13a'), + fa_html5('\uf13b'), + fa_css3('\uf13c'), + fa_anchor('\uf13d'), + fa_unlock_alt('\uf13e'), + fa_bullseye('\uf140'), + fa_ellipsis_h('\uf141'), + fa_ellipsis_v('\uf142'), + fa_rss_square('\uf143'), + fa_play_circle('\uf144'), + fa_ticket('\uf145'), + fa_minus_square('\uf146'), + fa_minus_square_o('\uf147'), + fa_level_up('\uf148'), + fa_level_down('\uf149'), + fa_check_square('\uf14a'), + fa_pencil_square('\uf14b'), + fa_external_link_square('\uf14c'), + fa_share_square('\uf14d'), + fa_compass('\uf14e'), + fa_toggle_down('\uf150'), + fa_caret_square_o_down('\uf150'), + fa_toggle_up('\uf151'), + fa_caret_square_o_up('\uf151'), + fa_toggle_right('\uf152'), + fa_caret_square_o_right('\uf152'), + fa_euro('\uf153'), + fa_eur('\uf153'), + fa_gbp('\uf154'), + fa_dollar('\uf155'), + fa_usd('\uf155'), + fa_rupee('\uf156'), + fa_inr('\uf156'), + fa_cny('\uf157'), + fa_rmb('\uf157'), + fa_yen('\uf157'), + fa_jpy('\uf157'), + fa_ruble('\uf158'), + fa_rouble('\uf158'), + fa_rub('\uf158'), + fa_won('\uf159'), + fa_krw('\uf159'), + fa_bitcoin('\uf15a'), + fa_btc('\uf15a'), + fa_file('\uf15b'), + fa_file_text('\uf15c'), + fa_sort_alpha_asc('\uf15d'), + fa_sort_alpha_desc('\uf15e'), + fa_sort_amount_asc('\uf160'), + fa_sort_amount_desc('\uf161'), + fa_sort_numeric_asc('\uf162'), + fa_sort_numeric_desc('\uf163'), + fa_thumbs_up('\uf164'), + fa_thumbs_down('\uf165'), + fa_youtube_square('\uf166'), + fa_youtube('\uf167'), + fa_xing('\uf168'), + fa_xing_square('\uf169'), + fa_youtube_play('\uf16a'), + fa_dropbox('\uf16b'), + fa_stack_overflow('\uf16c'), + fa_instagram('\uf16d'), + fa_flickr('\uf16e'), + fa_adn('\uf170'), + fa_bitbucket('\uf171'), + fa_bitbucket_square('\uf172'), + fa_tumblr('\uf173'), + fa_tumblr_square('\uf174'), + fa_long_arrow_down('\uf175'), + fa_long_arrow_up('\uf176'), + fa_long_arrow_left('\uf177'), + fa_long_arrow_right('\uf178'), + fa_apple('\uf179'), + fa_windows('\uf17a'), + fa_android('\uf17b'), + fa_linux('\uf17c'), + fa_dribbble('\uf17d'), + fa_skype('\uf17e'), + fa_foursquare('\uf180'), + fa_trello('\uf181'), + fa_female('\uf182'), + fa_male('\uf183'), + fa_gittip('\uf184'), + fa_gratipay('\uf184'), + fa_sun_o('\uf185'), + fa_moon_o('\uf186'), + fa_archive('\uf187'), + fa_bug('\uf188'), + fa_vk('\uf189'), + fa_weibo('\uf18a'), + fa_renren('\uf18b'), + fa_pagelines('\uf18c'), + fa_stack_exchange('\uf18d'), + fa_arrow_circle_o_right('\uf18e'), + fa_arrow_circle_o_left('\uf190'), + fa_toggle_left('\uf191'), + fa_caret_square_o_left('\uf191'), + fa_dot_circle_o('\uf192'), + fa_wheelchair('\uf193'), + fa_vimeo_square('\uf194'), + fa_turkish_lira('\uf195'), + fa_try('\uf195'), + fa_plus_square_o('\uf196'), + fa_space_shuttle('\uf197'), + fa_slack('\uf198'), + fa_envelope_square('\uf199'), + fa_wordpress('\uf19a'), + fa_openid('\uf19b'), + fa_institution('\uf19c'), + fa_bank('\uf19c'), + fa_university('\uf19c'), + fa_mortar_board('\uf19d'), + fa_graduation_cap('\uf19d'), + fa_yahoo('\uf19e'), + fa_google('\uf1a0'), + fa_reddit('\uf1a1'), + fa_reddit_square('\uf1a2'), + fa_stumbleupon_circle('\uf1a3'), + fa_stumbleupon('\uf1a4'), + fa_delicious('\uf1a5'), + fa_digg('\uf1a6'), + fa_pied_piper('\uf1a7'), + fa_pied_piper_alt('\uf1a8'), + fa_drupal('\uf1a9'), + fa_joomla('\uf1aa'), + fa_language('\uf1ab'), + fa_fax('\uf1ac'), + fa_building('\uf1ad'), + fa_child('\uf1ae'), + fa_paw('\uf1b0'), + fa_spoon('\uf1b1'), + fa_cube('\uf1b2'), + fa_cubes('\uf1b3'), + fa_behance('\uf1b4'), + fa_behance_square('\uf1b5'), + fa_steam('\uf1b6'), + fa_steam_square('\uf1b7'), + fa_recycle('\uf1b8'), + fa_automobile('\uf1b9'), + fa_car('\uf1b9'), + fa_cab('\uf1ba'), + fa_taxi('\uf1ba'), + fa_tree('\uf1bb'), + fa_spotify('\uf1bc'), + fa_deviantart('\uf1bd'), + fa_soundcloud('\uf1be'), + fa_database('\uf1c0'), + fa_file_pdf_o('\uf1c1'), + fa_file_word_o('\uf1c2'), + fa_file_excel_o('\uf1c3'), + fa_file_powerpoint_o('\uf1c4'), + fa_file_photo_o('\uf1c5'), + fa_file_picture_o('\uf1c5'), + fa_file_image_o('\uf1c5'), + fa_file_zip_o('\uf1c6'), + fa_file_archive_o('\uf1c6'), + fa_file_sound_o('\uf1c7'), + fa_file_audio_o('\uf1c7'), + fa_file_movie_o('\uf1c8'), + fa_file_video_o('\uf1c8'), + fa_file_code_o('\uf1c9'), + fa_vine('\uf1ca'), + fa_codepen('\uf1cb'), + fa_jsfiddle('\uf1cc'), + fa_life_bouy('\uf1cd'), + fa_life_buoy('\uf1cd'), + fa_life_saver('\uf1cd'), + fa_support('\uf1cd'), + fa_life_ring('\uf1cd'), + fa_circle_o_notch('\uf1ce'), + fa_ra('\uf1d0'), + fa_rebel('\uf1d0'), + fa_ge('\uf1d1'), + fa_empire('\uf1d1'), + fa_git_square('\uf1d2'), + fa_git('\uf1d3'), + fa_y_combinator_square('\uf1d4'), + fa_yc_square('\uf1d4'), + fa_hacker_news('\uf1d4'), + fa_tencent_weibo('\uf1d5'), + fa_qq('\uf1d6'), + fa_wechat('\uf1d7'), + fa_weixin('\uf1d7'), + fa_send('\uf1d8'), + fa_paper_plane('\uf1d8'), + fa_send_o('\uf1d9'), + fa_paper_plane_o('\uf1d9'), + fa_history('\uf1da'), + fa_circle_thin('\uf1db'), + fa_header('\uf1dc'), + fa_paragraph('\uf1dd'), + fa_sliders('\uf1de'), + fa_share_alt('\uf1e0'), + fa_share_alt_square('\uf1e1'), + fa_bomb('\uf1e2'), + fa_soccer_ball_o('\uf1e3'), + fa_futbol_o('\uf1e3'), + fa_tty('\uf1e4'), + fa_binoculars('\uf1e5'), + fa_plug('\uf1e6'), + fa_slideshare('\uf1e7'), + fa_twitch('\uf1e8'), + fa_yelp('\uf1e9'), + fa_newspaper_o('\uf1ea'), + fa_wifi('\uf1eb'), + fa_calculator('\uf1ec'), + fa_paypal('\uf1ed'), + fa_google_wallet('\uf1ee'), + fa_cc_visa('\uf1f0'), + fa_cc_mastercard('\uf1f1'), + fa_cc_discover('\uf1f2'), + fa_cc_amex('\uf1f3'), + fa_cc_paypal('\uf1f4'), + fa_cc_stripe('\uf1f5'), + fa_bell_slash('\uf1f6'), + fa_bell_slash_o('\uf1f7'), + fa_trash('\uf1f8'), + fa_copyright('\uf1f9'), + fa_at('\uf1fa'), + fa_eyedropper('\uf1fb'), + fa_paint_brush('\uf1fc'), + fa_birthday_cake('\uf1fd'), + fa_area_chart('\uf1fe'), + fa_pie_chart('\uf200'), + fa_line_chart('\uf201'), + fa_lastfm('\uf202'), + fa_lastfm_square('\uf203'), + fa_toggle_off('\uf204'), + fa_toggle_on('\uf205'), + fa_bicycle('\uf206'), + fa_bus('\uf207'), + fa_ioxhost('\uf208'), + fa_angellist('\uf209'), + fa_cc('\uf20a'), + fa_shekel('\uf20b'), + fa_sheqel('\uf20b'), + fa_ils('\uf20b'), + fa_meanpath('\uf20c'), + fa_buysellads('\uf20d'), + fa_connectdevelop('\uf20e'), + fa_dashcube('\uf210'), + fa_forumbee('\uf211'), + fa_leanpub('\uf212'), + fa_sellsy('\uf213'), + fa_shirtsinbulk('\uf214'), + fa_simplybuilt('\uf215'), + fa_skyatlas('\uf216'), + fa_cart_plus('\uf217'), + fa_cart_arrow_down('\uf218'), + fa_diamond('\uf219'), + fa_ship('\uf21a'), + fa_user_secret('\uf21b'), + fa_motorcycle('\uf21c'), + fa_street_view('\uf21d'), + fa_heartbeat('\uf21e'), + fa_venus('\uf221'), + fa_mars('\uf222'), + fa_mercury('\uf223'), + fa_intersex('\uf224'), + fa_transgender('\uf224'), + fa_transgender_alt('\uf225'), + fa_venus_double('\uf226'), + fa_mars_double('\uf227'), + fa_venus_mars('\uf228'), + fa_mars_stroke('\uf229'), + fa_mars_stroke_v('\uf22a'), + fa_mars_stroke_h('\uf22b'), + fa_neuter('\uf22c'), + fa_genderless('\uf22d'), + fa_facebook_official('\uf230'), + fa_pinterest_p('\uf231'), + fa_whatsapp('\uf232'), + fa_server('\uf233'), + fa_user_plus('\uf234'), + fa_user_times('\uf235'), + fa_hotel('\uf236'), + fa_bed('\uf236'), + fa_viacoin('\uf237'), + fa_train('\uf238'), + fa_subway('\uf239'), + fa_medium('\uf23a'), + fa_yc('\uf23b'), + fa_y_combinator('\uf23b'), + fa_optin_monster('\uf23c'), + fa_opencart('\uf23d'), + fa_expeditedssl('\uf23e'), + fa_battery_4('\uf240'), + fa_battery_full('\uf240'), + fa_battery_3('\uf241'), + fa_battery_three_quarters('\uf241'), + fa_battery_2('\uf242'), + fa_battery_half('\uf242'), + fa_battery_1('\uf243'), + fa_battery_quarter('\uf243'), + fa_battery_0('\uf244'), + fa_battery_empty('\uf244'), + fa_mouse_pointer('\uf245'), + fa_i_cursor('\uf246'), + fa_object_group('\uf247'), + fa_object_ungroup('\uf248'), + fa_sticky_note('\uf249'), + fa_sticky_note_o('\uf24a'), + fa_cc_jcb('\uf24b'), + fa_cc_diners_club('\uf24c'), + fa_clone('\uf24d'), + fa_balance_scale('\uf24e'), + fa_hourglass_o('\uf250'), + fa_hourglass_1('\uf251'), + fa_hourglass_start('\uf251'), + fa_hourglass_2('\uf252'), + fa_hourglass_half('\uf252'), + fa_hourglass_3('\uf253'), + fa_hourglass_end('\uf253'), + fa_hourglass('\uf254'), + fa_hand_grab_o('\uf255'), + fa_hand_rock_o('\uf255'), + fa_hand_stop_o('\uf256'), + fa_hand_paper_o('\uf256'), + fa_hand_scissors_o('\uf257'), + fa_hand_lizard_o('\uf258'), + fa_hand_spock_o('\uf259'), + fa_hand_pointer_o('\uf25a'), + fa_hand_peace_o('\uf25b'), + fa_trademark('\uf25c'), + fa_registered('\uf25d'), + fa_creative_commons('\uf25e'), + fa_gg('\uf260'), + fa_gg_circle('\uf261'), + fa_tripadvisor('\uf262'), + fa_odnoklassniki('\uf263'), + fa_odnoklassniki_square('\uf264'), + fa_get_pocket('\uf265'), + fa_wikipedia_w('\uf266'), + fa_safari('\uf267'), + fa_chrome('\uf268'), + fa_firefox('\uf269'), + fa_opera('\uf26a'), + fa_internet_explorer('\uf26b'), + fa_tv('\uf26c'), + fa_television('\uf26c'), + fa_contao('\uf26d'), + fa_500px('\uf26e'), + fa_amazon('\uf270'), + fa_calendar_plus_o('\uf271'), + fa_calendar_minus_o('\uf272'), + fa_calendar_times_o('\uf273'), + fa_calendar_check_o('\uf274'), + fa_industry('\uf275'), + fa_map_pin('\uf276'), + fa_map_signs('\uf277'), + fa_map_o('\uf278'), + fa_map('\uf279'), + fa_commenting('\uf27a'), + fa_commenting_o('\uf27b'), + fa_houzz('\uf27c'), + fa_vimeo('\uf27d'), + fa_black_tie('\uf27e'), + fa_fonticons('\uf280'), + fa_reddit_alien('\uf281'), + fa_edge('\uf282'), + fa_credit_card_alt('\uf283'), + fa_codiepie('\uf284'), + fa_modx('\uf285'), + fa_fort_awesome('\uf286'), + fa_usb('\uf287'), + fa_product_hunt('\uf288'), + fa_mixcloud('\uf289'), + fa_scribd('\uf28a'), + fa_pause_circle('\uf28b'), + fa_pause_circle_o('\uf28c'), + fa_stop_circle('\uf28d'), + fa_stop_circle_o('\uf28e'), + fa_shopping_bag('\uf290'), + fa_shopping_basket('\uf291'), + fa_hashtag('\uf292'), + fa_bluetooth('\uf293'), + fa_bluetooth_b('\uf294'), + fa_percent('\uf295'); + + char character; + + FontAwesomeIcons(char character) { + this.character = character; + } + + @Override + public String key() { + return name().replace('_', '-'); + } + + @Override + public char character() { + return character; + } +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/fonts/FontAwesomeModule.java b/iconify/src/main/java/com/joanzapata/iconify/fonts/FontAwesomeModule.java new file mode 100644 index 0000000000..0f2dfce4d4 --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/fonts/FontAwesomeModule.java @@ -0,0 +1,17 @@ +package com.joanzapata.iconify.fonts; + +import com.joanzapata.iconify.Icon; +import com.joanzapata.iconify.IconFontDescriptor; + +public class FontAwesomeModule implements IconFontDescriptor { + + @Override + public String ttfFileName() { + return "iconify/android-iconify-fontawesome.ttf"; + } + + @Override + public Icon[] characters() { + return FontAwesomeIcons.values(); + } +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/internal/CustomTypefaceSpan.java b/iconify/src/main/java/com/joanzapata/iconify/internal/CustomTypefaceSpan.java new file mode 100644 index 0000000000..a07bb49ef3 --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/internal/CustomTypefaceSpan.java @@ -0,0 +1,88 @@ +package com.joanzapata.iconify.internal; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.text.style.ReplacementSpan; +import com.joanzapata.iconify.Icon; + +public class CustomTypefaceSpan extends ReplacementSpan { + private static final int ROTATION_DURATION = 2000; + private static final Rect TEXT_BOUNDS = new Rect(); + private static final Paint LOCAL_PAINT = new Paint(); + private static final float BASELINE_RATIO = 1 / 7f; + + private final String icon; + private final Typeface type; + private final float iconSizePx; + private final float iconSizeRatio; + private final int iconColor; + private final boolean rotate; + private final boolean baselineAligned; + private final long rotationStartTime; + + public CustomTypefaceSpan(Icon icon, Typeface type, + float iconSizePx, float iconSizeRatio, int iconColor, + boolean rotate, boolean baselineAligned) { + this.rotate = rotate; + this.baselineAligned = baselineAligned; + this.icon = String.valueOf(icon.character()); + this.type = type; + this.iconSizePx = iconSizePx; + this.iconSizeRatio = iconSizeRatio; + this.iconColor = iconColor; + this.rotationStartTime = System.currentTimeMillis(); + } + + @Override + public int getSize(Paint paint, CharSequence text, + int start, int end, Paint.FontMetricsInt fm) { + LOCAL_PAINT.set(paint); + applyCustomTypeFace(LOCAL_PAINT, type); + LOCAL_PAINT.getTextBounds(icon, 0, 1, TEXT_BOUNDS); + if (fm != null) { + float baselineRatio = baselineAligned ? 0 : BASELINE_RATIO; + fm.descent = (int) (TEXT_BOUNDS.height() * baselineRatio); + fm.ascent = -(TEXT_BOUNDS.height() - fm.descent); + fm.top = fm.ascent; + fm.bottom = fm.descent; + } + return TEXT_BOUNDS.width(); + } + + @Override + public void draw(Canvas canvas, CharSequence text, + int start, int end, float x, int top, int y, + int bottom, Paint paint) { + applyCustomTypeFace(paint, type); + paint.getTextBounds(icon, 0, 1, TEXT_BOUNDS); + canvas.save(); + float baselineRatio = baselineAligned ? 0f : BASELINE_RATIO; + if (rotate) { + float rotation = (System.currentTimeMillis() - rotationStartTime) / (float) ROTATION_DURATION * 360f; + float centerX = x + TEXT_BOUNDS.width() / 2f; + float centerY = y - TEXT_BOUNDS.height() / 2f + TEXT_BOUNDS.height() * baselineRatio; + canvas.rotate(rotation, centerX, centerY); + } + + canvas.drawText(icon, + x - TEXT_BOUNDS.left, + y - TEXT_BOUNDS.bottom + TEXT_BOUNDS.height() * baselineRatio, paint); + canvas.restore(); + } + + public boolean isAnimated() { + return rotate; + } + + private void applyCustomTypeFace(Paint paint, Typeface tf) { + paint.setFakeBoldText(false); + paint.setTextSkewX(0f); + paint.setTypeface(tf); + if (rotate) paint.clearShadowLayer(); + if (iconSizeRatio > 0) paint.setTextSize(paint.getTextSize() * iconSizeRatio); + else if (iconSizePx > 0) paint.setTextSize(iconSizePx); + if (iconColor < Integer.MAX_VALUE) paint.setColor(iconColor); + } +} \ No newline at end of file diff --git a/iconify/src/main/java/com/joanzapata/iconify/internal/HasOnViewAttachListener.java b/iconify/src/main/java/com/joanzapata/iconify/internal/HasOnViewAttachListener.java new file mode 100644 index 0000000000..773c2a6949 --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/internal/HasOnViewAttachListener.java @@ -0,0 +1,82 @@ +package com.joanzapata.iconify.internal; + +import android.widget.TextView; + +import androidx.core.view.ViewCompat; + +/** + * Any TextView subclass that wishes to call {@link com.joanzapata.iconify.Iconify#addIcons(TextView...)} on it + * needs to implement this interface if it ever want to use spinning icons. + *
+ * IconTextView, IconButton and IconToggleButton already implement it, but if you need to implement it + * yourself, please use {@link com.joanzapata.iconify.internal.HasOnViewAttachListener.HasOnViewAttachListenerDelegate} + * to help you. + */ +public interface HasOnViewAttachListener { + void setOnViewAttachListener(OnViewAttachListener listener); + + interface OnViewAttachListener { + void onAttach(); + + void onDetach(); + } + + /** + * Helper class to implement {@link HasOnViewAttachListener}. + * Usual implementation should look like this: + *

+     * {@code
+     * class MyClass extends TextView implements HasOnViewAttachListener {
+     *
+     *       private HasOnViewAttachListenerDelegate delegate
+     *
+     *       @Override
+     *       public void setOnViewAttachListener(OnViewAttachListener listener) {
+     *          if (delegate == null) delegate = new HasOnViewAttachListenerDelegate(this);
+     *          delegate.setOnViewAttachListener(listener);
+     *       }
+     *
+     *       @Override
+     *       protected void onAttachedToWindow() {
+     *          super.onAttachedToWindow();
+     *          delegate.onAttachedToWindow();
+     *       }
+     *
+     *       @Override
+     *       protected void onDetachedFromWindow() {
+     *          super.onDetachedFromWindow();
+     *          delegate.onDetachedFromWindow();
+     *      }
+     *
+     *  }
+     * }
+     * 
+ */ + class HasOnViewAttachListenerDelegate { + + private final TextView view; + private OnViewAttachListener listener; + + public HasOnViewAttachListenerDelegate(TextView view) { + this.view = view; + } + + public void setOnViewAttachListener(OnViewAttachListener listener) { + if (this.listener != null) + this.listener.onDetach(); + this.listener = listener; + if (ViewCompat.isAttachedToWindow(view) && listener != null) { + listener.onAttach(); + } + } + + public void onAttachedToWindow() { + if (listener != null) listener.onAttach(); + } + + public void onDetachedFromWindow() { + if (listener != null) listener.onDetach(); + } + + } +} \ No newline at end of file diff --git a/iconify/src/main/java/com/joanzapata/iconify/internal/IconFontDescriptorWrapper.java b/iconify/src/main/java/com/joanzapata/iconify/internal/IconFontDescriptorWrapper.java new file mode 100644 index 0000000000..b7e1d0045b --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/internal/IconFontDescriptorWrapper.java @@ -0,0 +1,49 @@ +package com.joanzapata.iconify.internal; + +import android.content.Context; +import android.graphics.Typeface; +import com.joanzapata.iconify.Icon; +import com.joanzapata.iconify.IconFontDescriptor; + +import java.util.HashMap; +import java.util.Map; + +public class IconFontDescriptorWrapper { + + private final IconFontDescriptor iconFontDescriptor; + + private final Map iconsByKey; + + private Typeface cachedTypeface; + + public IconFontDescriptorWrapper(IconFontDescriptor iconFontDescriptor) { + this.iconFontDescriptor = iconFontDescriptor; + iconsByKey = new HashMap(); + Icon[] characters = iconFontDescriptor.characters(); + for (int i = 0, charactersLength = characters.length; i < charactersLength; i++) { + Icon icon = characters[i]; + iconsByKey.put(icon.key(), icon); + } + } + + public Icon getIcon(String key) { + return iconsByKey.get(key); + } + + public IconFontDescriptor getIconFontDescriptor() { + return iconFontDescriptor; + } + + public Typeface getTypeface(Context context) { + if (cachedTypeface != null) return cachedTypeface; + synchronized (this) { + if (cachedTypeface != null) return cachedTypeface; + cachedTypeface = Typeface.createFromAsset(context.getAssets(), iconFontDescriptor.ttfFileName()); + return cachedTypeface; + } + } + + public boolean hasIcon(Icon icon) { + return iconsByKey.values().contains(icon); + } +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/internal/ParsingUtil.java b/iconify/src/main/java/com/joanzapata/iconify/internal/ParsingUtil.java new file mode 100644 index 0000000000..574e7bbedf --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/internal/ParsingUtil.java @@ -0,0 +1,217 @@ +package com.joanzapata.iconify.internal; + +import java.util.List; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.TypedValue; +import android.widget.TextView; + +import androidx.core.view.ViewCompat; + +import com.joanzapata.iconify.Icon; +import com.joanzapata.iconify.internal.HasOnViewAttachListener.OnViewAttachListener; + +public final class ParsingUtil { + + private static final String ANDROID_PACKAGE_NAME = "android"; + + // Prevents instantiation + private ParsingUtil() {} + + public static CharSequence parse( + Context context, + List iconFontDescriptors, + CharSequence text, + final TextView target) { + context = context.getApplicationContext(); + + // Don't do anything related to iconify if text is null + if (text == null) return text; + + // Analyse the text and replace {} blocks with the appropriate character + // Retain all transformations in the accumulator + final SpannableStringBuilder spannableBuilder = new SpannableStringBuilder(text); + recursivePrepareSpannableIndexes(context, + text.toString(), spannableBuilder, + iconFontDescriptors, 0); + boolean isAnimated = hasAnimatedSpans(spannableBuilder); + + // If animated, periodically invalidate the TextView so that the + // CustomTypefaceSpan can redraw itself + if (isAnimated) { + if (target == null) + throw new IllegalArgumentException("You can't use \"spin\" without providing the target TextView."); + if (!(target instanceof HasOnViewAttachListener)) + throw new IllegalArgumentException(target.getClass().getSimpleName() + " does not implement " + + "HasOnViewAttachListener. Please use IconTextView, IconButton or IconToggleButton."); + + ((HasOnViewAttachListener) target).setOnViewAttachListener(new OnViewAttachListener() { + boolean isAttached = false; + + @Override + public void onAttach() { + isAttached = true; + ViewCompat.postOnAnimation(target, new Runnable() { + @Override + public void run() { + if (isAttached) { + target.invalidate(); + ViewCompat.postOnAnimation(target, this); + } + } + }); + } + + @Override + public void onDetach() { + isAttached = false; + } + }); + + } else if (target instanceof HasOnViewAttachListener) { + ((HasOnViewAttachListener) target).setOnViewAttachListener(null); + } + + return spannableBuilder; + } + + private static boolean hasAnimatedSpans(SpannableStringBuilder spannableBuilder) { + CustomTypefaceSpan[] spans = spannableBuilder.getSpans(0, spannableBuilder.length(), CustomTypefaceSpan.class); + for (CustomTypefaceSpan span : spans) { + if (span.isAnimated()) + return true; + } + return false; + } + + private static void recursivePrepareSpannableIndexes( + Context context, + String fullText, + SpannableStringBuilder text, + List iconFontDescriptors, + int start) { + + // Try to find a {...} in the string and extract expression from it + String stringText = text.toString(); + int startIndex = stringText.indexOf("{", start); + if (startIndex == -1) return; + int endIndex = stringText.indexOf("}", startIndex) + 1; + if (endIndex == -1) return; + String expression = stringText.substring(startIndex + 1, endIndex - 1); + + // Split the expression and retrieve the icon key + String[] strokes = expression.split(" "); + String key = strokes[0]; + + // Loop through the descriptors to find a key match + IconFontDescriptorWrapper iconFontDescriptor = null; + Icon icon = null; + for (int i = 0; i < iconFontDescriptors.size(); i++) { + iconFontDescriptor = iconFontDescriptors.get(i); + icon = iconFontDescriptor.getIcon(key); + if (icon != null) break; + } + + // If no match, ignore and continue + if (icon == null) { + recursivePrepareSpannableIndexes(context, fullText, text, iconFontDescriptors, endIndex); + return; + } + + // See if any more stroke within {} should be applied + float iconSizePx = -1; + int iconColor = Integer.MAX_VALUE; + float iconSizeRatio = -1; + boolean spin = false; + boolean baselineAligned = false; + for (int i = 1; i < strokes.length; i++) { + String stroke = strokes[i]; + + // Look for "spin" + if (stroke.equalsIgnoreCase("spin")) { + spin = true; + } + + // Look for "baseline" + else if (stroke.equalsIgnoreCase("baseline")) { + baselineAligned = true; + } + + // Look for an icon size + else if (stroke.matches("([0-9]*(\\.[0-9]*)?)dp")) { + iconSizePx = dpToPx(context, Float.valueOf(stroke.substring(0, stroke.length() - 2))); + } else if (stroke.matches("([0-9]*(\\.[0-9]*)?)sp")) { + iconSizePx = spToPx(context, Float.valueOf(stroke.substring(0, stroke.length() - 2))); + } else if (stroke.matches("([0-9]*)px")) { + iconSizePx = Integer.valueOf(stroke.substring(0, stroke.length() - 2)); + } else if (stroke.matches("@dimen/(.*)")) { + iconSizePx = getPxFromDimen(context, context.getPackageName(), stroke.substring(7)); + if (iconSizePx < 0) + throw new IllegalArgumentException("Unknown resource " + stroke + " in \"" + fullText + "\""); + } else if (stroke.matches("@android:dimen/(.*)")) { + iconSizePx = getPxFromDimen(context, ANDROID_PACKAGE_NAME, stroke.substring(15)); + if (iconSizePx < 0) + throw new IllegalArgumentException("Unknown resource " + stroke + " in \"" + fullText + "\""); + } else if (stroke.matches("([0-9]*(\\.[0-9]*)?)%")) { + iconSizeRatio = Float.valueOf(stroke.substring(0, stroke.length() - 1)) / 100f; + } + + // Look for an icon color + else if (stroke.matches("#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})")) { + iconColor = Color.parseColor(stroke); + } else if (stroke.matches("@color/(.*)")) { + iconColor = getColorFromResource(context, context.getPackageName(), stroke.substring(7)); + if (iconColor == Integer.MAX_VALUE) + throw new IllegalArgumentException("Unknown resource " + stroke + " in \"" + fullText + "\""); + } else if (stroke.matches("@android:color/(.*)")) { + iconColor = getColorFromResource(context, ANDROID_PACKAGE_NAME, stroke.substring(15)); + if (iconColor == Integer.MAX_VALUE) + throw new IllegalArgumentException("Unknown resource " + stroke + " in \"" + fullText + "\""); + } else { + throw new IllegalArgumentException("Unknown expression " + stroke + " in \"" + fullText + "\""); + } + } + + // Replace the character and apply the typeface + text = text.replace(startIndex, endIndex, "" + icon.character()); + text.setSpan(new CustomTypefaceSpan(icon, + iconFontDescriptor.getTypeface(context), + iconSizePx, iconSizeRatio, iconColor, spin, baselineAligned), + startIndex, startIndex + 1, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + recursivePrepareSpannableIndexes(context, fullText, text, iconFontDescriptors, startIndex); + } + + public static float getPxFromDimen(Context context, String packageName, String resName) { + Resources resources = context.getResources(); + int resId = resources.getIdentifier( + resName, "dimen", + packageName); + if (resId <= 0) return -1; + return resources.getDimension(resId); + } + + public static int getColorFromResource(Context context, String packageName, String resName) { + Resources resources = context.getResources(); + int resId = resources.getIdentifier( + resName, "color", + packageName); + if (resId <= 0) return Integer.MAX_VALUE; + return resources.getColor(resId); + } + + public static float dpToPx(Context context, float dp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, + context.getResources().getDisplayMetrics()); + } + + public static float spToPx(Context context, float sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, + context.getResources().getDisplayMetrics()); + } + +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/widget/IconButton.java b/iconify/src/main/java/com/joanzapata/iconify/widget/IconButton.java new file mode 100644 index 0000000000..8f5ee12ecf --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/widget/IconButton.java @@ -0,0 +1,55 @@ +package com.joanzapata.iconify.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; +import com.joanzapata.iconify.Iconify; +import com.joanzapata.iconify.internal.HasOnViewAttachListener; + +public class IconButton extends Button implements HasOnViewAttachListener { + + private HasOnViewAttachListenerDelegate delegate; + + public IconButton(Context context) { + super(context); + init(); + } + + public IconButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public IconButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + setTransformationMethod(null); + } + + @Override + public void setText(CharSequence text, BufferType type) { + super.setText(Iconify.compute(getContext(), text, this), type); + } + + + @Override + public void setOnViewAttachListener(HasOnViewAttachListener.OnViewAttachListener listener) { + if (delegate == null) delegate = new HasOnViewAttachListenerDelegate(this); + delegate.setOnViewAttachListener(listener); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + delegate.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + delegate.onDetachedFromWindow(); + } +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/widget/IconTextView.java b/iconify/src/main/java/com/joanzapata/iconify/widget/IconTextView.java new file mode 100644 index 0000000000..00a3b04c54 --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/widget/IconTextView.java @@ -0,0 +1,54 @@ +package com.joanzapata.iconify.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; +import com.joanzapata.iconify.Iconify; +import com.joanzapata.iconify.internal.HasOnViewAttachListener; + +public class IconTextView extends TextView implements HasOnViewAttachListener { + + private HasOnViewAttachListenerDelegate delegate; + + public IconTextView(Context context) { + super(context); + init(); + } + + public IconTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public IconTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + setTransformationMethod(null); + } + + @Override + public void setText(CharSequence text, BufferType type) { + super.setText(Iconify.compute(getContext(), text, this), type); + } + + @Override + public void setOnViewAttachListener(OnViewAttachListener listener) { + if (delegate == null) delegate = new HasOnViewAttachListenerDelegate(this); + delegate.setOnViewAttachListener(listener); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + delegate.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + delegate.onDetachedFromWindow(); + } +} diff --git a/iconify/src/main/java/com/joanzapata/iconify/widget/IconToggleButton.java b/iconify/src/main/java/com/joanzapata/iconify/widget/IconToggleButton.java new file mode 100644 index 0000000000..4bdb9ed5fe --- /dev/null +++ b/iconify/src/main/java/com/joanzapata/iconify/widget/IconToggleButton.java @@ -0,0 +1,55 @@ +package com.joanzapata.iconify.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ToggleButton; +import com.joanzapata.iconify.Iconify; +import com.joanzapata.iconify.internal.HasOnViewAttachListener; + +public class IconToggleButton extends ToggleButton implements HasOnViewAttachListener { + + private HasOnViewAttachListenerDelegate delegate; + + public IconToggleButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + public IconToggleButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public IconToggleButton(Context context) { + super(context); + init(); + } + + private void init() { + setTransformationMethod(null); + } + + @Override + public void setText(CharSequence text, BufferType type) { + super.setText(Iconify.compute(getContext(), text, this), BufferType.NORMAL); + } + + @Override + public void setOnViewAttachListener(HasOnViewAttachListener.OnViewAttachListener listener) { + if (delegate == null) delegate = new HasOnViewAttachListenerDelegate(this); + delegate.setOnViewAttachListener(listener); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + delegate.onAttachedToWindow(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + delegate.onDetachedFromWindow(); + } + +} diff --git a/medtronic/build.gradle b/medtronic/build.gradle index 89b2f37c51..de6506e40d 100644 --- a/medtronic/build.gradle +++ b/medtronic/build.gradle @@ -17,4 +17,5 @@ dependencies { implementation project(':pump-common') implementation project(':rileylink') implementation project(':shared') + implementation project(':iconify') } diff --git a/omnipod-common/build.gradle b/omnipod-common/build.gradle index 3d4aab407b..e1fccf18b1 100644 --- a/omnipod-common/build.gradle +++ b/omnipod-common/build.gradle @@ -16,4 +16,5 @@ android { dependencies { implementation project(':core') implementation project(':shared') + implementation project(':iconify') } \ No newline at end of file diff --git a/omnipod-dash/build.gradle b/omnipod-dash/build.gradle index 631dec2207..307befd0aa 100644 --- a/omnipod-dash/build.gradle +++ b/omnipod-dash/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation project(':omnipod-common') implementation project(':database') implementation project(':shared') + implementation project(':iconify') api "androidx.room:room-ktx:$room_version" api "androidx.room:room-runtime:$room_version" diff --git a/omnipod-eros/build.gradle b/omnipod-eros/build.gradle index a748715b9d..595ed441d0 100644 --- a/omnipod-eros/build.gradle +++ b/omnipod-eros/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation project(':rileylink') implementation project(':database') implementation project(':shared') - + implementation project(':iconify') api "androidx.room:room-ktx:$room_version" api "androidx.room:room-runtime:$room_version" diff --git a/settings.gradle b/settings.gradle index db9ca9d97f..20cc1cc2b6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,3 +18,4 @@ include ':diaconn' include ':openhumans' include ':shared' +include ':iconify'