1、概述
學習android少不了模仿各種app的界面,自從微信6.0問世以後,就覺得微信切換時那個變色的tab圖示屌屌的,今天我就帶大家自定義控件,帶你變色變得飛起~~
好了,下面先看下效果圖:
清晰度不太好,大家湊合看~~有木有覺得這個顔色弱爆了了的,,,下面我動動手指給你換個顔色:
有沒有這個顔色比較妖一點~~~好了~下面開始介紹原理。
2、原理介紹
通過上面的效果圖,大家可能也猜到了,我們的圖示并非是兩張圖檔,而是一張圖,并且目标顔色是可定制的,誰讓現在動不動就談個性化呢。
那麼我們如何做到,可以讓圖示随心所遇的變色了,其實原理,在我的部落格中出現了很多次了,下面你将看到一張熟悉的圖:
有沒有很熟悉的感腳,我們實際上還是利用了paint的xfermode,這次我們使用的是:mode.dst_in
dst_in回顧一下什麼效果,先繪制dst,設定mode,再繪制src,則顯示的是先後繪圖的交集區域,且是dst.
再仔細觀察下我們的圖示:
為了友善大家的觀看,我特意拿ps選擇了一下我們圖示的非透明區域,可以看到,我們這個小機器人非透明區域就是被線框起來的部分。
然後,我們圖示變色的原理就出現了:
1、先繪制一個顔色(例如:粉紅)
2、設定mode=dst_in
3、繪制我們這個可愛的小機器人
回答我,顯示什麼,是不是顯示交集,交集是什麼?交集是我們的小機器人的非透明區域,也就是那張臉,除了兩個眼;
好了,那怎麼變色呢?
我繪制一個顔色的時候,難道不能設定alpha麼~~~
到此,大家應該已經了解了我們圖示的繪制的原理了吧。
如果你對mode不熟悉:建議移步至:android 自定義控件實作刮刮卡效果 真的就隻是刮刮卡麼
3、自定義圖示控件
我們的整個界面不用說,是viewpager+fragment ,現在關注的是底部~~
接下來我們考慮,底部的tab,tab我們的布局是linearlayout,内部四個view,通過設定weight達到均分~~
這個view就是我們的自定義的圖示控件了,我們叫做:changecoloriconwithtextview
接下來考慮,應該有什麼屬性公布出來
想了一下,我決定把圖示,圖示顔色,圖示下顯示的文字,文字大小這四個屬性作為自定義屬性。
那就自定義屬性走起了:
a、values/attr.xml
[java] view plain copy
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="icon" format="reference" />
<attr name="color" format="color" />
<attr name="text" format="string" />
<attr name="text_size" format="dimension" />
<declare-styleable name="changecoloriconview">
<attr name="icon" />
<attr name="color" />
<attr name="text" />
<attr name="text_size" />
</declare-styleable>
</resources>
b、在布局檔案中使用
[html] view plain copy
<com.zhy.weixin6.ui.changecoloriconwithtextview
android:id="@+id/id_indicator_one"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_start_conversation"
zhy:text="@string/tab_weixin"
zhy:text_size="12sp" />
自己注意命名空間的寫法,xmlns:zhy="http://schemas.android.com/apk/res/應用的包名"。
c、在構造方法中擷取
public class changecoloriconwithtextview extends view
{
private bitmap mbitmap;
private canvas mcanvas;
private paint mpaint;
/**
* 顔色
*/
private int mcolor = 0xff45c01a;
* 透明度 0.0-1.0
private float malpha = 0f;
* 圖示
private bitmap miconbitmap;
* 限制繪制icon的範圍
private rect miconrect;
* icon底部文本
private string mtext = "微信";
private int mtextsize = (int) typedvalue.applydimension(
typedvalue.complex_unit_sp, 10, getresources().getdisplaymetrics());
private paint mtextpaint;
private rect mtextbound = new rect();
public changecoloriconwithtextview(context context)
{
super(context);
}
* 初始化自定義屬性值
*
* @param context
* @param attrs
public changecoloriconwithtextview(context context, attributeset attrs)
super(context, attrs);
// 擷取設定的圖示
typedarray a = context.obtainstyledattributes(attrs,
r.styleable.changecoloriconview);
int n = a.getindexcount();
for (int i = 0; i < n; i++)
{
int attr = a.getindex(i);
switch (attr)
{
case r.styleable.changecoloriconview_icon:
bitmapdrawable drawable = (bitmapdrawable) a.getdrawable(attr);
miconbitmap = drawable.getbitmap();
break;
case r.styleable.changecoloriconview_color:
mcolor = a.getcolor(attr, 0x45c01a);
case r.styleable.changecoloriconview_text:
mtext = a.getstring(attr);
case r.styleable.changecoloriconview_text_size:
mtextsize = (int) a.getdimension(attr, typedvalue
.applydimension(typedvalue.complex_unit_sp, 10,
getresources().getdisplaymetrics()));
}
}
a.recycle();
mtextpaint = new paint();
mtextpaint.settextsize(mtextsize);
mtextpaint.setcolor(0xff555555);
// 得到text繪制範圍
mtextpaint.gettextbounds(mtext, 0, mtext.length(), mtextbound);
可以看到,我們在構造方法中擷取了自定義的屬性,并且計算了文本占據的控件存在我們的mtextbound中。
我們考慮下,有了屬性,我們需要繪制一個文本,文本之上一個圖示,我們怎麼去控制繪制的區域呢?
我們的view顯示區域,無非以下三種情況:
針對這三種情況,我門的圖示的邊長應該是什麼呢?
我覺得邊長應該是:控件的高度-文本的高度-内邊距 與 控件的寬度-内邊距 兩者的小值;大家仔細推敲一下;
好了,有了上面的邊長的結論,我們就開始計算圖示的繪制範圍了:
@override
protected void onmeasure(int widthmeasurespec, int heightmeasurespec)
super.onmeasure(widthmeasurespec, heightmeasurespec);
// 得到繪制icon的寬
int bitmapwidth = math.min(getmeasuredwidth() - getpaddingleft()
- getpaddingright(), getmeasuredheight() - getpaddingtop()
- getpaddingbottom() - mtextbound.height());
int left = getmeasuredwidth() / 2 - bitmapwidth / 2;
int top = (getmeasuredheight() - mtextbound.height()) / 2 - bitmapwidth
/ 2;
// 設定icon的繪制範圍
miconrect = new rect(left, top, left + bitmapwidth, top + bitmapwidth);
}
繪制圖示有很多步驟呀,我來列一列
1、計算alpha(預設為0)
2、繪制原圖
3、在繪圖區域,繪制一個純色塊(設定了alpha),此步繪制在記憶體的bitmap上
4、設定mode,針對記憶體中的bitmap上的paint
5、繪制我們的圖示,此步繪制在記憶體的bitmap上
6、繪制原文本
7、繪制設定alpha和顔色後的文本
8、将記憶體中的bitmap繪制出來
根據上面的步驟,可以看出來,我們的圖示其實繪制了兩次,為什麼要繪制原圖呢,因為我覺得比較好看。
3-5步驟,就是我們上面分析的原理
6-7步,是繪制文本,可以看到,我們的文本就是通過設定alpha實作的
protected void ondraw(canvas canvas)
int alpha = (int) math.ceil((255 * malpha));
canvas.drawbitmap(miconbitmap, null, miconrect, null);
setuptargetbitmap(alpha);
drawsourcetext(canvas, alpha);
drawtargettext(canvas, alpha);
canvas.drawbitmap(mbitmap, 0, 0, null);
private void setuptargetbitmap(int alpha)
mbitmap = bitmap.createbitmap(getmeasuredwidth(), getmeasuredheight(),
config.argb_8888);
mcanvas = new canvas(mbitmap);
mpaint = new paint();
mpaint.setcolor(mcolor);
mpaint.setantialias(true);
mpaint.setdither(true);
mpaint.setalpha(alpha);
mcanvas.drawrect(miconrect, mpaint);
mpaint.setxfermode(new porterduffxfermode(porterduff.mode.dst_in));
mpaint.setalpha(255);
mcanvas.drawbitmap(miconbitmap, null, miconrect, mpaint);
private void drawsourcetext(canvas canvas, int alpha)
mtextpaint.setcolor(0xff333333);
mtextpaint.setalpha(255 - alpha);
canvas.drawtext(mtext, miconrect.left + miconrect.width() / 2
- mtextbound.width() / 2,
miconrect.bottom + mtextbound.height(), mtextpaint);
private void drawtargettext(canvas canvas, int alpha)
mtextpaint.setcolor(mcolor);
mtextpaint.setalpha(alpha);
關于繪制文本區域的計算,首先是起點x:miconrect.left + miconrect.width() / 2- mtextbound.width() / 2 有點長哈,文本miconrect.left + miconrect.width() / 2這個位置,在圖示水準區域的中心點,這個應該沒有疑問;圖示水準區域的中點- mtextbound.width() / 2 開始繪制文本,是不是就是居中在圖示的下面;
有人可能會問:你怎麼知道文本寬度小于圖示,我有5個字咋辦?5個字怎麼了,照樣是居中顯示,不信你試試~~
到此,我們的圖示控件寫完了,但是還沒有把我們的控制icon的方法放出去:
public void seticonalpha(float alpha)
this.malpha = alpha;
invalidateview();
private void invalidateview()
if (looper.getmainlooper() == looper.mylooper())
invalidate();
} else
postinvalidate();
我們叫做seticonalpha,避免了和setalpha沖突,設定完成後,invalidate一下~~~
到此就真的結束了,接下來看用法。
4、實戰
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:zhy="http://schemas.android.com/apk/res/com.zhy.weixin6.ui"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<android.support.v4.view.viewpager
android:id="@+id/id_viewpager"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1" >
</android.support.v4.view.viewpager>
<linearlayout
android:layout_height="60dp"
android:background="@drawable/tabbg"
android:orientation="horizontal" >
<com.zhy.weixin6.ui.changecoloriconwithtextview
android:id="@+id/id_indicator_one"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:padding="5dp"
zhy:icon="@drawable/ic_menu_start_conversation"
zhy:text="@string/tab_weixin"
zhy:text_size="12sp" />
android:id="@+id/id_indicator_two"
zhy:icon="@drawable/ic_menu_friendslist"
zhy:text="@string/tab_contact"
android:id="@+id/id_indicator_three"
zhy:icon="@drawable/ic_menu_emoticons"
zhy:text="@string/tab_find"
android:id="@+id/id_indicator_four"
zhy:icon="@drawable/ic_menu_allfriends"
zhy:text="@string/tab_me"
</linearlayout>
</linearlayout>
package com.zhy.weixin6.ui;
import java.lang.reflect.field;
import java.lang.reflect.method;
import java.util.arraylist;
import java.util.list;
import android.annotation.suppresslint;
import android.os.bundle;
import android.support.v4.app.fragment;
import android.support.v4.app.fragmentactivity;
import android.support.v4.app.fragmentpageradapter;
import android.support.v4.view.viewpager;
import android.support.v4.view.viewpager.onpagechangelistener;
import android.view.menu;
import android.view.view;
import android.view.view.onclicklistener;
import android.view.viewconfiguration;
import android.view.window;
@suppresslint("newapi")
public class mainactivity extends fragmentactivity implements
onpagechangelistener, onclicklistener
private viewpager mviewpager;
private list<fragment> mtabs = new arraylist<fragment>();
private fragmentpageradapter madapter;
private string[] mtitles = new string[] { "first fragment!",
"second fragment!", "third fragment!", "fourth fragment!" };
private list<changecoloriconwithtextview> mtabindicator = new arraylist<changecoloriconwithtextview>();
@override
protected void oncreate(bundle savedinstancestate)
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
setoverflowshowingalways();
getactionbar().setdisplayshowhomeenabled(false);
mviewpager = (viewpager) findviewbyid(r.id.id_viewpager);
initdatas();
mviewpager.setadapter(madapter);
mviewpager.setonpagechangelistener(this);
private void initdatas()
for (string title : mtitles)
tabfragment tabfragment = new tabfragment();
bundle args = new bundle();
args.putstring("title", title);
tabfragment.setarguments(args);
mtabs.add(tabfragment);
madapter = new fragmentpageradapter(getsupportfragmentmanager())
@override
public int getcount()
return mtabs.size();
public fragment getitem(int arg0)
return mtabs.get(arg0);
};
inittabindicator();
public boolean oncreateoptionsmenu(menu menu)
getmenuinflater().inflate(r.menu.main, menu);
return true;
private void inittabindicator()
changecoloriconwithtextview one = (changecoloriconwithtextview) findviewbyid(r.id.id_indicator_one);
changecoloriconwithtextview two = (changecoloriconwithtextview) findviewbyid(r.id.id_indicator_two);
changecoloriconwithtextview three = (changecoloriconwithtextview) findviewbyid(r.id.id_indicator_three);
changecoloriconwithtextview four = (changecoloriconwithtextview) findviewbyid(r.id.id_indicator_four);
mtabindicator.add(one);
mtabindicator.add(two);
mtabindicator.add(three);
mtabindicator.add(four);
one.setonclicklistener(this);
two.setonclicklistener(this);
three.setonclicklistener(this);
four.setonclicklistener(this);
one.seticonalpha(1.0f);
public void onpageselected(int arg0)
public void onpagescrolled(int position, float positionoffset,
int positionoffsetpixels)
// log.e("tag", "position = " + position + " , positionoffset = "
// + positionoffset);
if (positionoffset > 0)
changecoloriconwithtextview left = mtabindicator.get(position);
changecoloriconwithtextview right = mtabindicator.get(position + 1);
left.seticonalpha(1 - positionoffset);
right.seticonalpha(positionoffset);
public void onpagescrollstatechanged(int state)
public void onclick(view v)
resetothertabs();
switch (v.getid())
case r.id.id_indicator_one:
mtabindicator.get(0).seticonalpha(1.0f);
mviewpager.setcurrentitem(0, false);
break;
case r.id.id_indicator_two:
mtabindicator.get(1).seticonalpha(1.0f);
mviewpager.setcurrentitem(1, false);
case r.id.id_indicator_three:
mtabindicator.get(2).seticonalpha(1.0f);
mviewpager.setcurrentitem(2, false);
case r.id.id_indicator_four:
mtabindicator.get(3).seticonalpha(1.0f);
mviewpager.setcurrentitem(3, false);
* 重置其他的tab
private void resetothertabs()
for (int i = 0; i < mtabindicator.size(); i++)
mtabindicator.get(i).seticonalpha(0);
public boolean onmenuopened(int featureid, menu menu)
if (featureid == window.feature_action_bar && menu != null)
if (menu.getclass().getsimplename().equals("menubuilder"))
try
{
method m = menu.getclass().getdeclaredmethod(
"setoptionaliconsvisible", boolean.type);
m.setaccessible(true);
m.invoke(menu, true);
} catch (exception e)
}
return super.onmenuopened(featureid, menu);
private void setoverflowshowingalways()
try
// true if a permanent menu key is present, false otherwise.
viewconfiguration config = viewconfiguration.get(this);
field menukeyfield = viewconfiguration.class
.getdeclaredfield("shaspermanentmenukey");
menukeyfield.setaccessible(true);
menukeyfield.setboolean(config, false);
} catch (exception e)
e.printstacktrace();
activity裡面代碼雖然沒什麼注釋,但是很簡單哈,就是初始化fragment,得到我們的擴充卡,然後設定給viewpager;
inittabindicator我們初始化我們的自定義控件,以及加上了點選事件;
唯一一個需要指出的就是:
我們在onpagescrolled中,動态的擷取position以及positionoffset,然後拿到左右兩個view,設定positionoffset ;
這裡表示下慚愧,曾經在高仿微信5.2.1主界面架構 包含消息通知 的onpagescrolled中寫了一堆的if else,在視訊上線後,也有同學立馬就提出了,一行代碼搞定~~
是以,我們這裡簡單找了下規律,已經沒有if else的身影了~~~
還剩兩個反射的方法,是控制actionbar的圖示的,和點選menu按鍵,将actionbar的menu顯示在正常區域的~~
import android.graphics.color;
import android.view.gravity;
import android.view.layoutinflater;
import android.view.viewgroup;
import android.widget.textview;
public class tabfragment extends fragment
private string mtitle = "default";
public tabfragment()
public view oncreateview(layoutinflater inflater, viewgroup container,
bundle savedinstancestate)
if (getarguments() != null)
mtitle = getarguments().getstring("title");
textview textview = new textview(getactivity());
textview.settextsize(20);
textview.setbackgroundcolor(color.parsecolor("#ffffffff"));
textview.setgravity(gravity.center);
textview.settext(mtitle);
return textview;
好了,到此我們的整個案例就結束了~~
大家可以在布局檔案中設定各種顔色,4個不同顔色也可以,盡情的玩耍