項目要求:
筆者曾經(jīng)做過一個項目,其中登錄界面的交互令人印象深刻。交互設(shè)計師給出了一個非常作的設(shè)計,要求做出包含根據(jù)情況可變色的下劃線,左側(cè)有可變圖標(biāo),右側(cè)有可變刪除標(biāo)志的輸入框,如圖
Android 如何自定義EditText 下劃線?
記錄制作過程:
- 第一版本
public class LineEditText extends EditText {
private Paint mPaint;
private int color;
public static final int STATUS_FOCUSED = 1;
public static final int STATUS_UNFOCUSED = 2;
public static final int STATUS_ERROR = 3;
private int status = 2;
private Drawable del_btn;
private Drawable del_btn_down;
private int focusedDrawableId = R.drawable.user_select;// 默認(rèn)的
private int unfocusedDrawableId = R.drawable.user;
private int errorDrawableId = R.drawable.user_error;
Drawable left = null;
private Context mContext;
public LineEditText(Context context) {
super(context);
mContext = context;
init();
}
public LineEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
public LineEditText(Context context, AttributeSet attrs, int defStryle) {
super(context, attrs, defStryle);
mContext = context;
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.lineEdittext, defStryle, 0);
focusedDrawableId = a.getResourceId(
R.styleable.lineEdittext_drawableFocus, R.drawable.user_select);
unfocusedDrawableId = a.getResourceId(
R.styleable.lineEdittext_drawableUnFocus, R.drawable.user);
errorDrawableId = a.getResourceId(
R.styleable.lineEdittext_drawableError, R.drawable.user_error);
a.recycle();
init();
}
/**
* 2014/7/31
*
* @author Aimee.ZHANG
*/
private void init() {
mPaint = new Paint();
// mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(3.0f);
color = Color.parseColor("#bfbfbf");
setStatus(status);
del_btn = mContext.getResources().getDrawable(R.drawable.del_but_bg);
del_btn_down = mContext.getResources().getDrawable(R.drawable.del_but_bg_down);
addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence arg0, int arg1, int arg2,
int arg3) {
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1,
int arg2, int arg3) {
}
@Override
public void afterTextChanged(Editable arg0) {
setDrawable();
}
});
setDrawable();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(color);
canvas.drawLine(0, this.getHeight() - 1, this.getWidth(),
this.getHeight() - 1, mPaint);
}
// 刪除圖片
private void setDrawable() {
if (length() < 1) {
setCompoundDrawablesWithIntrinsicBounds(left, null, del_btn, null);
} else {
setCompoundDrawablesWithIntrinsicBounds(left, null, del_btn_down,null);
}
}
// 處理刪除事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (del_btn_down != null && event.getAction() == MotionEvent.ACTION_UP) {
int eventX = (int) event.getRawX();
int eventY = (int) event.getRawY();
Log.e("eventXY", "eventX = " + eventX + "; eventY = " + eventY);
Rect rect = new Rect();
getGlobalVisibleRect(rect);
rect.left = rect.right - 50;
if (rect.contains(eventX, eventY))
setText("");
}
return super.onTouchEvent(event);
}
public void setStatus(int status) {
this.status = status;
if (status == STATUS_ERROR) {
try {
left = getResources().getDrawable(errorDrawableId);
} catch (NotFoundException e) {
e.printStackTrace();
}
setColor(Color.parseColor("#f57272"));
} else if (status == STATUS_FOCUSED) {
try {
left = getResources().getDrawable(focusedDrawableId);
} catch (NotFoundException e) {
e.printStackTrace();
}
setColor(Color.parseColor("#5e99f3"));
} else {
try {
left = getResources().getDrawable(unfocusedDrawableId);
} catch (NotFoundException e) {
e.printStackTrace();
}
setColor(Color.parseColor("#bfbfbf"));
}
if (left != null) {
// left.setBounds(0, 0, 30, 40);
// this.setCompoundDrawables(left, null, null, null);
setCompoundDrawablesWithIntrinsicBounds(left,null,del_btn,null);
}
postInvalidate();
}
public void setLeftDrawable(int focusedDrawableId, int unfocusedDrawableId,
int errorDrawableId) {
this.focusedDrawableId = focusedDrawableId;
this.unfocusedDrawableId = unfocusedDrawableId;
this.errorDrawableId = errorDrawableId;
setStatus(status);
}
@Override
protected void onFocusChanged(boolean focused, int direction,
Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
if (focused) {
setStatus(STATUS_FOCUSED);
} else {
setStatus(STATUS_UNFOCUSED);
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
};
public void setColor(int color) {
this.color = color;
this.setTextColor(color);
invalidate();
}
}
效果圖:

代碼解釋:
變量名 STATUS_FOCUSED,STATUS_UNFOCUSED,STATUS_ERROR 標(biāo)示了三種狀態(tài),選中狀況為藍(lán)色,未選中狀態(tài)為灰色,錯誤狀態(tài)為紅色。focusedDrawableId unfocusedDrawableId errorDrawableId存放三種狀態(tài)的圖片,放置于最左側(cè)。
canvas.drawLine(0, this.getHeight() - 1, this.getWidth(),this.getHeight() - 1, mPaint); //畫editText最下方的線
setCompoundDrawablesWithIntrinsicBounds(left, null, del_btn, null); //放置左邊的和右邊的圖片(左,上,右,下)
相當(dāng)于 android:drawableLeft="" android:drawableRight=""
- onTouchEvent 當(dāng)手機(jī)點(diǎn)擊時,第一個先執(zhí)行的函數(shù),當(dāng)點(diǎn)擊右側(cè)刪除圖標(biāo)是清空 edittext
- setStatus 根據(jù)不同的狀態(tài),左邊的圖片不一樣
存在的問題:
這版本雖然基本功能已經(jīng)實現(xiàn),但是不符合需求,設(shè)計中要求文本框中無文字時,右側(cè)刪除按鈕不顯示,不點(diǎn)擊刪除按鈕,刪除按鈕要保持灰色,點(diǎn)擊時才可以變藍(lán)色。
因此有了第二個版本
public class LineEditText extends EditText implements TextWatcher, OnFocusChangeListener{
private Paint mPaint;
private int color;
public static final int STATUS_FOCUSED = 1;
public static final int STATUS_UNFOCUSED = 2;
public static final int STATUS_ERROR = 3;
private int status = 2;
private Drawable del_btn;
private Drawable del_btn_down;
private int focusedDrawableId = R.drawable.user_select;// 默認(rèn)的
private int unfocusedDrawableId = R.drawable.user;
private int errorDrawableId = R.drawable.user_error;
Drawable left = null;
private Context mContext;
/**
* 是否獲取焦點(diǎn),默認(rèn)沒有焦點(diǎn)
*/
private boolean hasFocus = false;
/**
* 手指抬起時的X坐標(biāo)
*/
private int xUp = 0;
public LineEditText(Context context) {
super(context);
mContext = context;
init();
}
public LineEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
public LineEditText(Context context, AttributeSet attrs, int defStryle) {
super(context, attrs, defStryle);
mContext = context;
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.lineEdittext, defStryle, 0);
focusedDrawableId = a.getResourceId(
R.styleable.lineEdittext_drawableFocus, R.drawable.user_select);
unfocusedDrawableId = a.getResourceId(
R.styleable.lineEdittext_drawableUnFocus, R.drawable.user);
errorDrawableId = a.getResourceId(
R.styleable.lineEdittext_drawableError, R.drawable.user_error);
a.recycle();
init();
}
/**
* 2014/7/31
*
* @author Aimee.ZHANG
*/
private void init() {
mPaint = new Paint();
// mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(3.0f);
color = Color.parseColor("#bfbfbf");
setStatus(status);
del_btn = mContext.getResources().getDrawable(R.drawable.del_but_bg);
del_btn_down = mContext.getResources().getDrawable(R.drawable.del_but_bg_down);
addListeners();
setCompoundDrawablesWithIntrinsicBounds(left, null, null, null);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(color);
canvas.drawLine(0, this.getHeight() - 1, this.getWidth(),
this.getHeight() - 1, mPaint);
}
// 刪除圖片
// private void setDrawable() {
// if (length() < 1) {
// setCompoundDrawablesWithIntrinsicBounds(left, null, null, null);
// } else {
// setCompoundDrawablesWithIntrinsicBounds(left, null, del_btn,null);
// }
// }
// 處理刪除事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (del_btn != null && event.getAction() == MotionEvent.ACTION_UP) {
// 獲取點(diǎn)擊時手指抬起的X坐標(biāo)
xUp = (int) event.getX();
Log.e("xUp", xUp+"");
/*Rect rect = new Rect();
getGlobalVisibleRect(rect);
rect.left = rect.right - 50;*/
// 當(dāng)點(diǎn)擊的坐標(biāo)到當(dāng)前輸入框右側(cè)的距離小于等于 getCompoundPaddingRight() 的距離時,則認(rèn)為是點(diǎn)擊了刪除圖標(biāo)
if ((getWidth() - xUp) <= getCompoundPaddingRight()) {
if (!TextUtils.isEmpty(getText().toString())) {
setText("");
}
}
}else if(del_btn != null && event.getAction() == MotionEvent.ACTION_DOWN && getText().length()!=0){
setCompoundDrawablesWithIntrinsicBounds(left,null,del_btn_down,null);
}else if(getText().length()!=0){
setCompoundDrawablesWithIntrinsicBounds(left,null,del_btn,null);
}
return super.onTouchEvent(event);
}
public void setStatus(int status) {
this.status = status;
if (status == STATUS_ERROR) {
try {
left = getResources().getDrawable(errorDrawableId);
} catch (NotFoundException e) {
e.printStackTrace();
}
setColor(Color.parseColor("#f57272"));
} else if (status == STATUS_FOCUSED) {
try {
left = getResources().getDrawable(focusedDrawableId);
} catch (NotFoundException e) {
e.printStackTrace();
}
setColor(Color.parseColor("#5e99f3"));
} else {
try {
left = getResources().getDrawable(unfocusedDrawableId);
} catch (NotFoundException e) {
e.printStackTrace();
}
setColor(Color.parseColor("#bfbfbf"));
}
if (left != null) {
// left.setBounds(0, 0, 30, 40);
// this.setCompoundDrawables(left, null, null, null);
setCompoundDrawablesWithIntrinsicBounds(left,null,null,null);
}
postInvalidate();
}
public void setLeftDrawable(int focusedDrawableId, int unfocusedDrawableId,
int errorDrawableId) {
this.focusedDrawableId = focusedDrawableId;
this.unfocusedDrawableId = unfocusedDrawableId;
this.errorDrawableId = errorDrawableId;
setStatus(status);
}
private void addListeners() {
try {
setOnFocusChangeListener(this);
addTextChangedListener(this);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onFocusChanged(boolean focused, int direction,
Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
this.hasFocus=focused;
if (focused) {
setStatus(STATUS_FOCUSED);
} else {
setStatus(STATUS_UNFOCUSED);
setCompoundDrawablesWithIntrinsicBounds(left,null,null,null);
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
};
public void setColor(int color) {
this.color = color;
this.setTextColor(color);
invalidate();
}
@Override
public void afterTextChanged(Editable arg0) {
// TODO Auto-generated method stub
postInvalidate();
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
int arg3) {
// TODO Auto-generated method stub
if (TextUtils.isEmpty(arg0)) {
// 如果為空,則不顯示刪除圖標(biāo)
setCompoundDrawablesWithIntrinsicBounds(left, null, null, null);
} else {
// 如果非空,則要顯示刪除圖標(biāo)
setCompoundDrawablesWithIntrinsicBounds(left, null, del_btn, null);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int after) {
if (hasFocus) {
if (TextUtils.isEmpty(s)) {
// 如果為空,則不顯示刪除圖標(biāo)
setCompoundDrawablesWithIntrinsicBounds(left, null, null, null);
} else {
// 如果非空,則要顯示刪除圖標(biāo)
setCompoundDrawablesWithIntrinsicBounds(left, null, del_btn, null);
}
}
}
@Override
public void onFocusChange(View arg0, boolean arg1) {
// TODO Auto-generated method stub
try {
this.hasFocus = arg1;
} catch (Exception e) {
e.printStackTrace();
}
}
}
比較關(guān)鍵的方法是:onTouchEvent
當(dāng)進(jìn)入界面,點(diǎn)擊輸入框,要判斷輸入框中是否已有文字,如果有則顯示灰色的刪除按鈕,如果沒有則不顯示,如果點(diǎn)擊了刪除按鈕,刪除按鈕變藍(lán)色
存在的問題:
這個版本依舊存在問題,就是輸入長度超過輸入框,所畫的線不會延伸,如圖

解決方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(color);
int x=this.getScrollX();
int w=this.getMeasuredWidth();
canvas.drawLine(0, this.getHeight() - 1, w+x,
this.getHeight() - 1, mPaint);
}
w:獲取控件長度
X:延伸后的長度
最終效果:

在分享完這個界面的代碼設(shè)計后,筆者跟大家嘮一些新玩意。話說身處在帝都,如果不利用好帝都的豐厚資源,又如何對得起每天吸入的幾十斤霧霾?
話嘮的分享
在帝都生活,我每天早晨起來都會告訴自己,又是新的一天,要認(rèn)真過。
寫一個 APP 很容易,寫好一個 APP 很難。如何檢驗自己所寫的 APP 的性能狀況,用戶體驗?
什么是 APM?
In the fields of information technology and systems management, Application Performance Management (APM) is the monitoring and management of performance and availability of software applications. APM strives to detect and diagnose complex application performance problems to maintain an expected level of service. APM is "the translation of IT metrics into business meaning .
國內(nèi)外有已很多成熟的 APM 廠商,筆者也曾染指過幾家,如AppDynamics,Newrelic,OneAPM
還有專注于 APP 崩潰監(jiān)控的產(chǎn)品:Crashlytics,Crittercism,Bugly等
今天我想給大家分享的是從OneAPM Mobile Insight 產(chǎn)品中發(fā)現(xiàn)的一塊新大陸--卡頓監(jiān)控
對流暢度的概念,相信大家并不陌生,即 1s 中之內(nèi)繪圖刷新信號中斷的次數(shù)。流暢度次數(shù)越接近 40 時,用戶能感知到卡頓,流暢度在 20以下卡頓比較嚴(yán)重。OneAPM Mobile Insight的卡頓監(jiān)控就是一個監(jiān)控流暢度指標(biāo)的模塊。

- 卡頓趨勢圖:隨時間的推移,反饋卡頓發(fā)生次數(shù)的趨勢情況
- 設(shè)備分布圖:卡頓現(xiàn)象集中分布的設(shè)備類型
- 卡頓頁面:發(fā)生卡頓的頁面有哪些,其中平均流暢度是多少,卡頓了多少次等信息。


查看單個頁面的卡頓情況,并從頁面線程加載的情況中分析造成卡頓原因
如果你也想檢驗一下自己所寫的 APP 的用戶體驗情況,不妨試試這個新玩意~~
OneAPM Mobile Insight 以真實用戶體驗為度量標(biāo)準(zhǔn)進(jìn)行 Crash 分析,監(jiān)控網(wǎng)絡(luò)請求及網(wǎng)絡(luò)錯誤,提升用戶留存。訪問 OneAPM 官方網(wǎng)站感受更多應(yīng)用性能優(yōu)化體驗,想閱讀更多技術(shù)文章,請訪問 OneAPM 官方技術(shù)博客。
本文轉(zhuǎn)自 OneAPM 官方博客
