Introduction
Ever wished that Android had a built-in Password EditText with a Show/Hide button? No? Well, here’s a tutorial for one anyway.
Building a custom Password Edit Text
In order to get a Password EditText we’ll first need to create a custom view extending a EditText. Let’s create a new PasswordEditText.java class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class PasswordEditText extends EditText { public PasswordEditText(Context context) { super(context); init(null); } public PasswordEditText(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public PasswordEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs); } |
Next, we’ll need to define some custom attributes for our view. Let’s start off by defining its: action text, colour and size. To make things easier, we’ll need to be able to define these attributes in the view’s layout.xml file. As a result, we’ll need to define these attributes as styleables, by creating a new /res/values/attrs.xml file:
1 2 3 4 5 6 7 8 |
<resources> <declare-styleable name="PasswordEditText"> <attr name="showActionText" format="string"/> <attr name="hideActionText" format="string"/> <attr name="actionLabelColor" format="color"/> <attr name="actionLabelSize" format="dimension"/> </declare-styleable> </resources> |
Next, we’ll need to associate these attributes with our custom view. So, let’s start off by giving them some default values:
1 2 3 4 5 6 7 8 9 |
private static final String TEXT_SHOW = "SHOW"; private static final String TEXT_HIDE = "HIDE"; private static final int ACTION_COLOR = Color.BLUE; private static final float ACTION_SIZE = 11.0f; private String showActionText; private String hideActionText; private int actionLabelColor; private float actionLabelDimension; |
We’ll now need to initialize them when our view is created:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
private void init(AttributeSet attrs) { //Set default attributes setActionLabelColor(ACTION_COLOR); setActionLabelDimension(ACTION_SIZE); if (attrs != null) { TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.PasswordEditText, 0, 0); try { setShowActionText(typedArray.getString(R.styleable.PasswordEditText_showActionText)); setHideActionText(typedArray.getString(R.styleable.PasswordEditText_hideActionText)); setActionLabelColor(typedArray.getColor(R.styleable.PasswordEditText_actionLabelColor, ACTION_COLOR)); setActionLabelDimension(typedArray.getDimension(R.styleable.PasswordEditText_actionLabelSize, ACTION_SIZE)); } finally { typedArray.recycle(); } } if (getShowActionText() == null) { setShowActionText(TEXT_SHOW); } if (getHideActionText() == null) { setHideActionText(TEXT_HIDE); } setTransformationMethod(new PasswordTransformationMethod()); setPasswordVisibilityDrawable(isPasswordVisible); } |
Awesome, all we need now is the Show/Hide action text. The EditText (being an extension of TextView) has built-in Compound Drawables and we’ll use those to house our action text. Since the end goal is to insert an editable message (SHOW / HIDE) in our custom view, we’ll just need to find a way to convert a String into a drawable – this is where the magic happens. We’ll create TextView and style it with our custom attributes (text, colour, size). Once we’re happy with it, we just essentially take a screenshot of it, convert that into a drawable and assign it as a Compound Drawable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
private void setPasswordVisibilityDrawable(boolean isPasswordVisible) { TextView textView = new TextView(getContext()); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, getActionLabelDimension()); textView.setTextColor(getActionLabelColor()); if (isPasswordVisible) { textView.setText(getHideActionText()); } else { textView.setText(getShowActionText()); } textView.setDrawingCacheEnabled(true); // Without this, the view will have a dimension of 0,0 and the bitmap will be null textView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); textView.buildDrawingCache(true); try { Bitmap bitmap = Bitmap.createBitmap(textView.getDrawingCache()); // clear drawing cache textView.setDrawingCacheEnabled(false); Drawable showHide = new BitmapDrawable(getResources(), bitmap); showHide.setBounds(0, 0, showHide.getIntrinsicWidth(), showHide.getIntrinsicHeight()); setCompoundDrawables(getCompoundDrawables()[0], getCompoundDrawables()[1], showHide, getCompoundDrawables()[3]); } catch (Exception e) { e.printStackTrace(); } } |
Great, now we’ve got a custom drawable inside our view. We just need a way to handle touch events to determine what happens when users click it. To do this, our custom view will need to implement a View.OnTouchListener to handle this event. Then, all we have to do is get our drawable’s dimensions and decide what happens when a touch event is registered within those coordinates:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class PasswordEditText extends EditText implements View.OnTouchListener { private OnTouchListener onTouchListener; @Override public void setOnTouchListener(OnTouchListener onTouchListener) { this.onTouchListener = onTouchListener; } @Override public boolean onTouch(View v, MotionEvent event) { if (getCompoundDrawables()[2] != null) { boolean tappedX = event.getX() > (getWidth() - getPaddingRight() - getCompoundDrawables()[2].getIntrinsicWidth()); if (tappedX) { if (event.getAction() == MotionEvent.ACTION_UP) { if (isPasswordVisible) { setTransformationMethod(new PasswordTransformationMethod()); isPasswordVisible = false; } else { setTransformationMethod(null); isPasswordVisible = true; } setSelection(getText().length()); setPasswordVisibilityDrawable(isPasswordVisible); } return true; } } if (onTouchListener != null) { return onTouchListener.onTouch(v, event); } return false; } .... private void init(AttributeSet attrs) { .... super.setOnTouchListener(this); } } |
Wrapping things up
And that’s basically it. We’ve created a custom Password EditText, defined it’s attributes, added a custom editable drawable and defined what happens when users tap it. This is how the final PasswordEditText.java class looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.method.PasswordTransformationMethod; import android.util.AttributeSet; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import android.widget.EditText; import android.widget.TextView; /** * Displays a Password EditText with a SHOW/HIDE button at the right-hand side. */ public class PasswordEditText extends EditText implements View.OnTouchListener { private static final String TEXT_SHOW = "SHOW"; private static final String TEXT_HIDE = "HIDE"; private static final int ACTION_COLOR = Color.BLUE; private static final float ACTION_SIZE = 11.0f; private OnTouchListener onTouchListener; private boolean isPasswordVisible = false; private String showActionText; private String hideActionText; private int actionLabelColor; private float actionLabelDimension; /** * Creates the PasswordEditText * * @param context Context */ public PasswordEditText(Context context) { super(context); init(null); } /** * Creates the PasswordEditText * * @param context Context * @param attrs AttributeSet */ public PasswordEditText(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } /** * Creates the PasswordEditText * * @param context Context * @param attrs AttributeSet * @param defStyle int */ public PasswordEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs); } public String getShowActionText() { return showActionText; } public void setShowActionText(String showActionText) { this.showActionText = showActionText; setPasswordVisibilityDrawable(isPasswordVisible); } public String getHideActionText() { return hideActionText; } public void setHideActionText(String hideActionText) { this.hideActionText = hideActionText; setPasswordVisibilityDrawable(isPasswordVisible); } public int getActionLabelColor() { return actionLabelColor; } public void setActionLabelColor(int actionLabelColor) { this.actionLabelColor = actionLabelColor; setPasswordVisibilityDrawable(isPasswordVisible); } public float getActionLabelDimension() { return actionLabelDimension; } public void setActionLabelDimension(float actionLabelDimension) { this.actionLabelDimension = actionLabelDimension; setPasswordVisibilityDrawable(isPasswordVisible); } @Override public void setOnTouchListener(OnTouchListener onTouchListener) { this.onTouchListener = onTouchListener; } @Override public boolean onTouch(View v, MotionEvent event) { if (getCompoundDrawables()[2] != null) { boolean tappedX = event.getX() > (getWidth() - getPaddingRight() - getCompoundDrawables()[2].getIntrinsicWidth()); if (tappedX) { if (event.getAction() == MotionEvent.ACTION_UP) { if (isPasswordVisible) { setTransformationMethod(new PasswordTransformationMethod()); isPasswordVisible = false; } else { setTransformationMethod(null); isPasswordVisible = true; } setSelection(getText().length()); setPasswordVisibilityDrawable(isPasswordVisible); } return true; } } if (onTouchListener != null) { return onTouchListener.onTouch(v, event); } return false; } private void init(AttributeSet attrs) { //Set default attributes setActionLabelColor(ACTION_COLOR); setActionLabelDimension(ACTION_SIZE); if (attrs != null) { TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.PasswordEditText, 0, 0); try { setShowActionText(typedArray.getString(R.styleable.PasswordEditText_showActionText)); setHideActionText(typedArray.getString(R.styleable.PasswordEditText_hideActionText)); setActionLabelColor(typedArray.getColor(R.styleable.PasswordEditText_actionLabelColor, ACTION_COLOR)); setActionLabelDimension(typedArray.getDimension(R.styleable.PasswordEditText_actionLabelSize, ACTION_SIZE)); } finally { typedArray.recycle(); } } if (getShowActionText() == null) { setShowActionText(TEXT_SHOW); } if (getHideActionText() == null) { setHideActionText(TEXT_HIDE); } setTransformationMethod(new PasswordTransformationMethod()); setPasswordVisibilityDrawable(isPasswordVisible); super.setOnTouchListener(this); } private void setPasswordVisibilityDrawable(boolean isPasswordVisible) { TextView textView = new TextView(getContext()); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, getActionLabelDimension()); textView.setTextColor(getActionLabelColor()); if (isPasswordVisible) { textView.setText(getHideActionText()); } else { textView.setText(getShowActionText()); } textView.setDrawingCacheEnabled(true); // Without this, the view will have a dimension of 0,0 and the bitmap will be null textView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); textView.buildDrawingCache(true); try { Bitmap bitmap = Bitmap.createBitmap(textView.getDrawingCache()); // clear drawing cache textView.setDrawingCacheEnabled(false); Drawable showHide = new BitmapDrawable(getResources(), bitmap); showHide.setBounds(0, 0, showHide.getIntrinsicWidth(), showHide.getIntrinsicHeight()); setCompoundDrawables(getCompoundDrawables()[0], getCompoundDrawables()[1], showHide, getCompoundDrawables()[3]); } catch (Exception e) { e.printStackTrace(); } } } |
We just need to add it to res/layout/activity_main.xml and see it in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.screechstuidos.passwordedittext.MainActivity"> <com.screechstuidos.passwordedittext.PasswordEditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" app:actionLabelColor="#009688" app:hideActionText="HIDE" app:showActionText="SHOW"/> </RelativeLayout> |
Notice that we’re setting our custom attributes in a XML layout file. We can also do the same programmatically:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class MainActivity extends Activity { private PasswordEditText passwordEditText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); passwordEditText = (PasswordEditText) findViewById(R.id.password); //Change Password Edit Text attributes programmatically passwordEditText.setShowActionText("UNMASK"); passwordEditText.setHideActionText("MASK"); passwordEditText.setActionLabelColor(Color.RED); passwordEditText.setActionLabelDimension(45); } } |
That’s it. Check out the full source code here.