From Right to Left and Back

From Right to Left and Back

Talk given at Droidcon SF 2016 about implementing RTL on Android.

Some of the topics discussed:
- overview of RTL
- how to implement RTL pre and post api-17
- implementing RTL in custom Views, ViewGroups, and Drawables
- common pitfalls and problems and how to solve them

credit for images:
- android flavor versions: https://www.android.com/history
- various meems: https://imgflip.com/memegenerator

F60e42d94f99f029b590206076dbd354?s=128

Ahmed El-Helw

March 17, 2016
Tweet

Transcript

  1. From Right to Left and Back @ahmedre | helw.net

  2. Why Support RTL?

  3. Potential Reach • Arabic: 295 million native speakers • Urdu:

    66 million native speakers • Persian: 45 million native speakers Nationalencyklopedin, 2010
  4. Statistics - Q1 2015 • 95% of phones shipped in

    MEA are iOS / Android • shipments of iOS/Android phones “increased by a combined 67% year on year” IDC Press Release, July 12th, 2015
  5. –IDC Press Release, July 12th, 2015 “In the Middle East,

    Android currently represents 80% of market's volume, while iOS accounts for 17%; in Africa, these figures stand at 89% and 7%, respectively.”
  6. RTL Overview

  7. Scripts • Arabic script ةيبرعلا • Hebrew script תירִבִע •

    Other scripts (Syriac, Thaana, etc)
  8. RTL • Font • Reshaper support • Layout mirroring •

    Bidirectional text support (Bidi) د م ح ا دمحأ
  9. Android

  10. Eclair and Froyo • layout-{ar,…} • no Arabic font, no

    reshaping
  11. Eclair and Froyo

  12. Gingerbread • Arabic font support • No reshaping

  13. Gingerbread

  14. Ice Cream Sandwich • Arabic reshaper! • Arabic localization

  15. Ice Cream Sandwich

  16. Jellybean • Bidi support - 4.1 • Layout mirroring support,

    improved fonts - 4.2 • Improved Bidi support - 4.3
  17. None
  18. Kitkat • Drawable Mirroring • Force RTL

  19. None
  20. RTL on API 17+

  21. android:supportsRTL otherwise, getLayoutDirection will always return LTR

  22. android:layoutDirection • LAYOUT_DIRECTION_LTR • LAYOUT_DIRECTION_RTL • LAYOUT_DIRECTION_INHERIT • LAYOUT_DIRECTION_LOCALE

  23. Resolving Layout Direction • android:supportsRTL - if not, return LTR

    • based on android:layoutDirection • LAYOUT_DIRECTION_RTL or LAYOUT_DIRECTION_LTR • LAYOUT_DIRECTION_LOCALE • LAYOUT_DIRECTION_INHERIT
  24. Resolving Layout Direction • for the inherit case, ViewRootImpl sets

    top level direction to use Locale. • resolving Locale happens in TextUtils (17+) • TextUtils gets information from ICU
  25. Mirroring Layouts

  26. <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello" android:gravity="left"/> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/hello" android:gravity="start"/>
  27. <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello" android:drawableLeft="@drawable/image"/> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/hello" android:drawableLeft="@drawable/image" android:drawableStart="@drawable/image"/>
  28. <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello" android:paddingLeft="@dimen/padding"/> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/hello" android:paddingLeft="@dimen/padding" android:paddingStart="@dimen/padding"/>
  29. <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello" android:layout_marginLeft="@dimen/margin"/> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"

    android:text="@string/hello" android:layout_marginLeft="@dimen/margin" android:layout_marginStart="@dimen/margin"/>
  30. Mirroring Layouts • RelativeLayout parameters • layout_alignParentStart, layout_alignParentEnd • layout_alignStart,layout_alignEnd

    • layout_toStartOf, layout_toEndOf
  31. Mirroring Layouts • ldrtl / ldltr resource qualifiers • lower

    priority than -lang • useful for mirroring drawables
  32. Building Android Components

  33. Supporting Gravity in ViewGroups

  34. if ((gravity & RELATIVE_LAYOUT_DIRECTION) > 0) { if ((gravity &

    Gravity.START) == Gravity.START) { if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { // start resolving to right } else { // start resolving to left } } else if ((gravity & Gravity.END) == Gravity.END) { if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { // end resolving to left } else { // end resolving to right } } } else if ((gravity & Gravity.LEFT) == Gravity.LEFT) { // left } else if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { // right }
  35. Gravity.getAbsoluteGravity( gravity, layoutDirection);

  36. android:gravity start end right left absolute gravity left right LTR

  37. android:gravity start end right left absolute gravity left right RTL

  38. gravity = Gravity.getAbsoluteGravity( gravity, getLayoutDirection()); switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {

    case Gravity.RIGHT: break; case Gravity.LEFT: default: break; }
  39. Top / Start

  40. Bottom / End

  41. Center

  42. Center Vertical / Start

  43. Gravity.apply( gravity, w, h, container, outRect, layoutDirection);

  44. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); gravity of whatever

    we want to position, ex Gravity.START | Gravity.TOP
  45. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); width and height

    of whatever we’re trying to position
  46. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); a Rect in

    which the item will be positioned. should be at least as large as the item.
  47. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); the frame of

    the item we’re trying to place
  48. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); the layout direction,

    for translating start and end
  49. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); child.layout(outRect.left, outRect.top, outRect.right,

    outRect.bottom);
  50. Gravity.apply( gravity, w, h, container, outRect, layoutDirection); drawable.setBounds(outRect);

  51. Reverse ordering of Children for RTL

  52. None
  53. LinearLayout int start = 0; int dir = 1; //

    In case of RTL, start drawing from the last child.
 if (isLayoutRtl) { start = count - 1; dir = -1;
 } 
 for (int i = 0; i < count; i++) { int childIndex = start + dir * i; final View child = getVirtualChildAt(childIndex); // ... }
  54. Handling RTL for Views

  55. None
  56. @Override protected void onDraw(Canvas canvas) { canvas.drawRect(0, 0, 100, 100,

    paint); }
  57. None
  58. @Override protected void onDraw(Canvas canvas) { boolean isRtl = getLayoutDirection()

    == LAYOUT_DIRECTION_RTL; if (isRtl) { canvas.save(); canvas.translate(getWidth(), 0); canvas.scale(-1.0f, 1.0f); } canvas.drawRect(0, 0, 100, 100, paint); if (isRtl) { canvas.restore(); } }
  59. Framework

  60. setAutoMirrored • Introduced in Kitkat (19+) • setAutoMirrored / android:autoMirrored

    • BitmapDrawable uses it to flip the bitmap
  61. ViewPager • no RTL support • reverse the order of

    the pages and map indices
  62. Supporting RTL pre-4.2

  63. Older APIs • no layout-ldrtl / layout-ldltr • have to

    fallback to layout-ar, layout-he, …
  64. Example <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="120dp"> <TextView

    android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/two" android:background="#E3F2FD"/> </LinearLayout> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/one" android:background="#E8F5E9"/>
  65. 4.1 4.2

  66. 4.1 4.2

  67. layout-ar <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="120dp"> <TextView

    android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/two" android:background="#E3F2FD"/> </LinearLayout> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/one" android:background="#E8F5E9"/>
  68. 4.1 4.2

  69. Solutions • android:layoutDirection=“ltr” • alternative: resource mirrors layout-ar-v17

  70. layout-ar <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="120dp" android:layout_direction="ltr">

    <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/one" android:background="#E8F5E9"/> </LinearLayout> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/two" android:background="#E3F2FD"/>
  71. None
  72. • for certain text pre-17, may need to explicitly set

    the gravity. • GravityCompat and ViewCompat are your friends
  73. Older APIs - pre-14 • include a font capable of

    rendering the language • bundle your own reshaper • be wary of custom vendor solutions
  74. None
  75. Common Problems

  76. RelativeLayout <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
 android:layout_height="match_parent"> <Button android:layout_width="wrap_content"

    android:layout_height="wrap_content" android:text="@string/app_name" android:layout_alignParentLeft="true" android:layout_alignParentStart="true"/>
 </RelativeLayout>
  77. 4.3 - api 18 4.2 - api 17

  78. 4.3 - api 18 4.2 - api 17

  79. RelativeLayout • same problem with right/end • Fixed in 4.3

    (api 18) • layout-v17 with android:alignParentStart
  80. Text

  81. Character Types • Strong a b ت ب • Weak

    + - : . , • Neutral whitespace • Explicit Formatting Characters
  82. Text Direction • TEXT_DIRECTION_INHERIT • TEXT_DIRECTION_FIRST_STRONG • TEXT_DIRECTION_ANY_RTL • TEXT_DIRECTION_LTR

    • TEXT_DIRECTION_RTL • TEXT_DIRECTION_LOCALE • TEXT_DIRECTION_FIRST_STRONG_LTR • TEXT_DIRECTION_FIRST_STRONG_RTL • TEXT_DIRECTION_INHERIT • TEXT_DIRECTION_FIRST_STRONG • TEXT_DIRECTION_ANY_RTL • TEXT_DIRECTION_LTR • TEXT_DIRECTION_RTL • TEXT_DIRECTION_LOCALE • TEXT_DIRECTION_FIRST_STRONG_LTR • TEXT_DIRECTION_FIRST_STRONG_RTL
  83. Text Direction • default is TEXT_DIRECTION_INHERIT • ViewRootImpl sets this

    to TEXT_DIRECTION_FIRST_STRONG
  84. None
  85. <?xml version="1.0" encoding="utf-8"?>
 <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/first" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#aaff" android:gravity="center_vertical"

    android:layout_gravity="center" android:padding="16dp" android:drawableLeft="@drawable/ic_launcher"
 android:drawableStart="@drawable/ic_launcher"/>
  86. @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); setContentView(R.layout.rtl_text); // Ahmed

    in English, ﺪﻤﺣأ in Arabic String name = getString(R.string.name); String s = getString(R.string.name_format); TextView tv = (TextView) findViewById(R.id.first); tv.setText(String.format(s, name, 3)); }
  87. None
  88. @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); setContentView(R.layout.rtl_text); // Ahmed

    in Arabic, ﺪﻤﺣأ in English String name = getString(R.string.name_opposite); String s = getString(R.string.name_format); TextView tv = (TextView) findViewById(R.id.first); tv.setText(String.format(s, name, 3)); }
  89. None
  90. @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); setContentView(R.layout.rtl_text); // Ahmed

    in Arabic, ﺪﻤﺣأ in English String name = getString(R.string.name_opposite); String s = getString(R.string.name_format); BidiFormatter formatter = BidiFormatter.getInstance();
 name = formatter.unicodeWrap(name); TextView tv = (TextView) findViewById(R.id.first); tv.setText(String.format(s, name, 3)); }
  91. None
  92. Tips

  93. Tips • android:supportsRTL=“true” • for layout parameters, add START and

    END in addition to RIGHT and LEFT • be careful when mixing RTL and LTR text
  94. Tips • Be wary when using the default locale •

    ship RTL to 17+ to save a lot of work • test layouts and views in RTL and LTR on various api versions
  95. None
  96. Thanks! @ahmedre / helw.net