概述
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么~~~
到此,大家应该已经了解了我们图标的绘制的原理了吧。
3、自定义图标控件
我们的整个界面不用说,是ViewPager+Fragment ,现在关注的是底部~~
接下来我们考虑,底部的Tab,Tab我们的布局是LinearLayout,内部四个View,通过设置weight达到均分~~
这个View就是我们的自定义的图标控件了,我们叫做:ChangeColorIconWithTextView
关键点
接下来考虑,应该有什么属性公布出来
1、自定义属性
想了一下,我决定把图标,图标颜色,图标下显示的文字,文字大小这四个属性作为自定义属性。
那就自定义属性走起了:
a、values/attr.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<? 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、在布局文件中使用
1
2
3
4
5
6
7
8
9
|
< 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、在构造方法中获取
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
|
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 ); break ; case R.styleable.ChangeColorIconView_text: mText = a.getString(attr); break ; case R.styleable.ChangeColorIconView_text_size: mTextSize = ( int ) a.getDimension(attr, TypedValue .applyDimension(TypedValue.COMPLEX_UNIT_SP, 10 , getResources().getDisplayMetrics())); break ; } } a.recycle(); mTextPaint = new Paint(); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor( 0xff555555 ); // 得到text绘制范围 mTextPaint.getTextBounds(mText, 0 , mText.length(), mTextBound); } |
可以看到,我们在构造方法中获取了自定义的属性,并且计算了文本占据的控件存在我们的mTextBound中。
2、图标的绘制区域的选择
我们考虑下,有了属性,我们需要绘制一个文本,文本之上一个图标,我们怎么去控制绘制的区域呢?
我们的View显示区域,无非以下三种情况:
针对这三种情况,我门的图标的边长应该是什么呢?
我觉得边长应该是:控件的高度-文本的高度-内边距 与 控件的宽度-内边距 两者的小值;大家仔细推敲一下;
好了,有了上面的边长的结论,我们就开始计算图标的绘制范围了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@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); } |
3、绘制图标
绘制图标有很多步骤呀,我来列一列
(1)计算alpha(默认为0)
(2)绘制原图
(3)在绘图区域,绘制一个纯色块(设置了alpha),此步绘制在内存的bitmap上
(4)设置mode,针对内存中的bitmap上的paint
(5)绘制我们的图标,此步绘制在内存的bitmap上
(6)绘制原文本
(7)绘制设置alpha和颜色后的文本
(8)将内存中的bitmap绘制出来
根据上面的步骤,可以看出来,我们的图标其实绘制了两次,为什么要绘制原图呢,因为我觉得比较好看。
3-5步骤,就是我们上面分析的原理
6-7步,是绘制文本,可以看到,我们的文本就是通过设置alpha实现的
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
|
@Override 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.setTextSize(mTextSize); 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); canvas.drawText(mText, mIconRect.left + mIconRect.width() / 2 - mTextBound.width() / 2 , mIconRect.bottom + mTextBound.height(), mTextPaint); } |
关于绘制文本区域的计算,首先是起点x:mIconRect.left + mIconRect.width() / 2- mTextBound.width() / 2 有点长哈,文本mIconRect.left + mIconRect.width() / 2这个位置,在图标水平区域的中心点,这个应该没有疑问;图标水平区域的中点- mTextBound.width() / 2 开始绘制文本,是不是就是居中在图标的下面;
有人可能会问:你怎么知道文本宽度小于图标,我有5个字咋办?5个字怎么了,照样是居中显示,不信你试试~~
4、公布设置透明度的方法
到此,我们的图标控件写完了,但是还没有把我们的控制icon的方法放出去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public void setIconAlpha( float alpha) { this .mAlpha = alpha; invalidateView(); } private void invalidateView() { if (Looper.getMainLooper() == Looper.myLooper()) { invalidate(); } else { postInvalidate(); } } |
我们叫做setIconAlpha,避免了和setAlpha冲突,设置完成后,invalidate一下~~~
到此就真的结束了,接下来看用法。
实战
1、布局文件
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
|
< 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_width = "fill_parent" 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" /> < com.zhy.weixin6.ui.ChangeColorIconWithTextView android:id = "@+id/id_indicator_two" android:layout_width = "0dp" android:layout_height = "fill_parent" android:layout_weight = "1" android:padding = "5dp" zhy:icon = "@drawable/ic_menu_friendslist" zhy:text = "@string/tab_contact" zhy:text_size = "12sp" /> < com.zhy.weixin6.ui.ChangeColorIconWithTextView android:id = "@+id/id_indicator_three" android:layout_width = "0dp" android:layout_height = "fill_parent" android:layout_weight = "1" android:padding = "5dp" zhy:icon = "@drawable/ic_menu_emoticons" zhy:text = "@string/tab_find" zhy:text_size = "12sp" /> < com.zhy.weixin6.ui.ChangeColorIconWithTextView android:id = "@+id/id_indicator_four" android:layout_width = "0dp" android:layout_height = "fill_parent" android:layout_weight = "1" android:padding = "5dp" zhy:icon = "@drawable/ic_menu_allfriends" zhy:text = "@string/tab_me" zhy:text_size = "12sp" /> </ LinearLayout > </ LinearLayout > |
2、MainActivity
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
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(); } @Override public Fragment getItem( int arg0) { return mTabs.get(arg0); } }; initTabIndicator(); } @Override 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); } @Override public void onPageSelected( int arg0) { } @Override 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); } } @Override public void onPageScrollStateChanged( int state) { } @Override 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 ); break ; case R.id.id_indicator_three: mTabIndicator.get( 2 ).setIconAlpha( 1 .0f); mViewPager.setCurrentItem( 2 , false ); break ; case R.id.id_indicator_four: mTabIndicator.get( 3 ).setIconAlpha( 1 .0f); mViewPager.setCurrentItem( 3 , false ); break ; } } /** * 重置其他的Tab */ private void resetOtherTabs() { for ( int i = 0 ; i < mTabIndicator.size(); i++) { mTabIndicator.get(i).setIconAlpha( 0 ); } } @Override 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显示在正常区域的~~
3、TabFragment
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
|
package com.zhy.weixin6.ui; import android.graphics.Color; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; public class TabFragment extends Fragment { private String mTitle = "Default" ; public TabFragment() { } @Override 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个不同颜色也可以,尽情的玩耍吧~~