高仿微信app实战(二)- 自定义View实现底部导航栏
来源:互联网 发布:mac qq怎么上传群文件 编辑:程序博客网 时间:2024/05/29 15:39
高仿微信app实战(二)- 自定义View实现底部导航栏
话不多说,先上效果图
本文要实现微信底部导航栏,通过自定义View实现。主界面的切换采用ViewPager+Fragement的方式实现。
一、自定义ColorfulRadioButton
由于要实现切换Tab过程中按钮颜色的渐变效果,所以通过自定义RadioButton有些困难,这里重新自定义一个View-ColorfulRadioButton。
首先写一下设计思路:
1、首先每个Button有两部分构成,分别是上方的icon图标和下方的文字,使用两个画笔分别绘制。
2、再onMesure中计算图标和文字的大小与绘制位置。
3、在onDraw中绘制View。通过两次绘制(第一次为灰色图标,第二次为带颜色和透明度图标)设置PorterDuffXfermode为DistIn实现背景色透明度的变化。不懂的可以参考这篇文章:PorterDuffXfermode(即图像混合模式)详解
(1)自定义View属性
列举一下我们的自定义View需要用户指定的属性
- Icon图标
- 底部文字
- 渐变颜色
- 字体大小
在value文件夹下attrs中声明这些属性
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="ColorfulRadioButton"> <attr name="icon" format="reference"></attr> <attr name="color" format="color"></attr> <attr name="text" format="string"></attr> <attr name="text_size" format="dimension"></attr></resources>
(2)实现自定义View
package com.example.sdk.widgets;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.PorterDuff;import android.graphics.PorterDuffXfermode;import android.graphics.Rect;import android.graphics.drawable.BitmapDrawable;import android.os.Looper;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.util.TypedValue;import android.view.KeyEvent;import android.view.SoundEffectConstants;import android.view.View;import android.view.ViewGroup;import android.view.ViewParent;import android.widget.Checkable;import android.widget.CompoundButton;import com.example.sdk.R;/** * Created by 韩冰 on 2017/7/12. */public class ColorfulRadioButton extends View implements Checkable{ public ColorfulRadioButton(Context context) { this(context,null); } public ColorfulRadioButton(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } private int mColor = 0xFF45C01A; //默认渐变颜色 private Bitmap mIconBitmap; private String mText = "Button"; //默认底部显示文字 private int mTextSize = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics()); //默认字体大小 private Canvas mCanvas; private Bitmap mBitmap; private Paint mPaint; private float mAlpha = 0; //透明度 private Rect mIconRect; //icon Rect private Rect mTextBound; //文字Rect private Paint mTextPaint; //绘制文字画笔 private OnCheckedChangeListener mOnCheckedChangedListener; //监听按钮状态变化 public ColorfulRadioButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorfulRadioButton); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); if (attr == R.styleable.ColorfulRadioButton_icon) { BitmapDrawable drawable = (BitmapDrawable) a.getDrawable(attr); mIconBitmap = drawable.getBitmap(); } else if (attr == R.styleable.ColorfulRadioButton_color) { mColor = a.getColor(attr, 0xFF45C01A); } else if (attr == R.styleable.ColorfulRadioButton_text) { mText = a.getString(attr); } else if (attr == R.styleable.ColorfulRadioButton_text_size) { mTextSize = (int) a.getDimension(attr, TypedValue .applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics())); } } a.recycle(); mTextBound = new Rect(); mTextPaint = new Paint(); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(0Xff555555); mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBound); setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { setChecked(true); } }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //icon宽度等于view的宽减去padding(或者view的高减去padding再减去文字的高度,取两者的最小值) int iconWidth = Math.min(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - mTextBound.height()); int left = getMeasuredWidth() / 2 - iconWidth / 2; //icon的左边界坐标 int top = getMeasuredHeight() / 2 - (mTextBound.height() + iconWidth ) / 2; //icon的右边界坐标 mIconRect = new Rect(left, top, left + iconWidth, top + iconWidth); } @Override protected void onDraw(Canvas canvas) { canvas.drawBitmap(mIconBitmap, null, mIconRect, null); int alpha = (int) Math.ceil(255 * mAlpha); // 内存去准备mBitmap , setAlpha , 纯色 ,xfermode , 图标 setupTargetBitmap(alpha); // 1、绘制原文本 ; 2、绘制变色的文本 drawSourceText(canvas, alpha); drawTargetText(canvas, alpha); canvas.drawBitmap(mBitmap, 0, 0, null); } /** * 绘制变色的文本 * * @param canvas * @param alpha */ private void drawTargetText(Canvas canvas, int alpha) { mTextPaint.setColor(mColor); mTextPaint.setAlpha(alpha); int x = getMeasuredWidth() / 2 - mTextBound.width() / 2; int y = mIconRect.bottom + mTextBound.height(); canvas.drawText(mText, x, y , mTextPaint); } /** * 绘制原文本 * * @param canvas * @param alpha */ private void drawSourceText(Canvas canvas, int alpha) { mTextPaint.setColor(0xff8D8D8D); mTextPaint.setAlpha(255 - alpha); int x = getMeasuredWidth() / 2 - mTextBound.width() / 2; int y = mIconRect.bottom + mTextBound.height(); canvas.drawText(mText, x, y, mTextPaint); } /** * 在内存中绘制可变色的Icon */ private void setupTargetBitmap(int alpha) { mBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.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); } public void setIconAlpha(float alpha) { this.mAlpha = alpha; invalidateView(); } /** * 重绘 */ private void invalidateView() { if (Looper.getMainLooper() == Looper.myLooper()) { invalidate(); } else { postInvalidate(); } } @Override public void setChecked(boolean checked) { setIconAlpha(checked ? 1:0); if (mOnCheckedChangedListener != null){ mOnCheckedChangedListener.onCheckedChanged(this, checked); } } @Override public boolean isChecked() { return mAlpha == 1; } @Override public void toggle() { setChecked(!isChecked()); } public void setmOnCheckedChangedListener(OnCheckedChangeListener listener){ this.mOnCheckedChangedListener = listener; } /** * Interface definition for a callback to be invoked when the checked state * of a compound button changed. */ public static interface OnCheckedChangeListener { /** * Called when the checked state of a compound button has changed. * * @param buttonView The button view whose state has changed. * @param isChecked The new checked state of buttonView. */ void onCheckedChanged(ColorfulRadioButton buttonView, boolean isChecked); }}
(三)自定义ColorfulRadioGroup
为了实现ViewPager滑动时颜色渐变的效果,需要对同一组的Button进行统一管理,这里的实现模仿了android RadioGroup API。这里setAlphaOffset方法是关键,它可以联动相邻的两个button实现颜色渐变效果。
package com.example.sdk.widgets;import android.content.Context;import android.content.res.TypedArray;import android.os.Build;import android.os.Bundle;import android.os.Parcelable;import android.support.annotation.IdRes;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;import android.widget.LinearLayout;import com.example.sdk.R;import com.example.sdk.utils.ViewUtils;import java.util.ArrayList;import java.util.List;/** * Created by 韩冰 on 2017/7/12. */public class ColorfulRadioGroup extends LinearLayout { public ColorfulRadioGroup(Context context) { super(context); setOrientation(HORIZONTAL); init(); } private int mCheckedId = -1; private int mCheckedPosition = -1; private OnCheckedChangeListener mOnCheckedChangeListener; public void setOnCheckedChangeListener(OnCheckedChangeListener listener){ this.mOnCheckedChangeListener = listener; } private boolean mProtectFromCheckedChange = false; private ColorfulRadioButton.OnCheckedChangeListener mChildOnCheckedChangeListener; //为child button设置的回调 private HierarchyChangeListener mHierarchyChangeListener; //加入新的button为其设置回调 private List<ColorfulRadioButton> mButtons; //属于这个组的Button数组 public ColorfulRadioGroup(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.ColorfulRadioGroup); int value = attributes.getResourceId(R.styleable.ColorfulRadioGroup_checkedButton, View.NO_ID); if (value != View.NO_ID) { mCheckedId = value; } attributes.recycle(); init(); } private void init(){ mChildOnCheckedChangeListener = new CheckedStateTracker(); mHierarchyChangeListener = new HierarchyChangeListener(); super.setOnHierarchyChangeListener(mHierarchyChangeListener); mButtons = new ArrayList<>(); } /** * {@inheritDoc} */ @Override protected void onFinishInflate() { //完成加载后设置默认选中的按钮 super.onFinishInflate(); // checks the appropriate radio button as requested in the XML file if (mCheckedId != -1) { mProtectFromCheckedChange = true; setCheckedStateForView(mCheckedId, true); mProtectFromCheckedChange = false; setCheckedId(mCheckedId); } } private void setCheckedId(@IdRes int id) { mCheckedId = id; mCheckedPosition = mButtons.indexOf(findViewById(mCheckedId)); if (mOnCheckedChangeListener != null) { mOnCheckedChangeListener.onCheckedChanged(this, mCheckedPosition); } } private void setCheckedStateForView(int viewId, boolean checked) { View checkedView = findViewById(viewId); if (checkedView != null && checkedView instanceof ColorfulRadioButton) { ((ColorfulRadioButton) checkedView).setChecked(checked); } } /** * //设置透明度,两个button中第一个button的位置为position的值, * @param position 第一个button的位置 * @param offset 偏移量,取值范围为0-1 */ public void setAlphaOffset(int position, float offset){ if (offset <= 0 || offset>=1){ return; } int index = mCheckedPosition; if (position == index || position == index-1){ mButtons.get(position).setIconAlpha(1-offset); mButtons.get(position+1).setIconAlpha(offset); } } /** * 设置选中button的位置 * @param position */ public void setCheckedPosition(int position){ if (position>=0 && position<mButtons.size()) mButtons.get(position).setChecked(true); } /** * 获得当前被选中button的位置 * @return */ public int getCheckedPosition(){ return mCheckedPosition; } /** * <p>Interface definition for a callback to be invoked when the checked * radio button changed in this group.</p> */ public interface OnCheckedChangeListener{ /** * <p>Called when the checked radio button has changed. When the * selection is cleared, checkedId is -1.</p> * * @param group the group in which the checked radio button has changed * @param position the unique identifier of the newly checked radio button */ void onCheckedChanged(ColorfulRadioGroup group, int position); } private class CheckedStateTracker implements ColorfulRadioButton.OnCheckedChangeListener{ /** * 如果某个按钮被选中,设置之前被选中按钮为被选中状态 * @param buttonView The button view whose state has changed. * @param isChecked The new checked state of buttonView. */ @Override public void onCheckedChanged(ColorfulRadioButton buttonView, boolean isChecked) { // prevents from infinite recursion if (mProtectFromCheckedChange) { return; } mProtectFromCheckedChange = true; if (mCheckedId != -1) { setCheckedStateForView(mCheckedId, false); } mProtectFromCheckedChange = false; int id = buttonView.getId(); setCheckedId(id); } } /** * */ private class HierarchyChangeListener implements ViewGroup.OnHierarchyChangeListener { /** * 当新的Button被加入时,为button设置id,设置回调,加入buttons数组 */ public void onChildViewAdded(View parent, View child) { if (parent == ColorfulRadioGroup.this && child instanceof ColorfulRadioButton) { int id = child.getId(); // generates an id if it's missing if (id == View.NO_ID) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) id = ViewUtils.generateViewId(); else id = View.generateViewId(); child.setId(id); } ((ColorfulRadioButton) child).setmOnCheckedChangedListener( mChildOnCheckedChangeListener); mButtons.add((ColorfulRadioButton) child); } } /** * 当Button被移除时,移除相应信息 */ public void onChildViewRemoved(View parent, View child) { if (parent == ColorfulRadioGroup.this && child instanceof ColorfulRadioButton) { if (child.getId() == mCheckedId){ mCheckedId = -1; mCheckedPosition = -1; } ((ColorfulRadioButton) child).setmOnCheckedChangedListener(null); mButtons.remove(child); } } }}
(四)、使用自定义ColorfulButton实现微信底部导航栏
布局文件
ViewPager+ColorfulRadioGroup
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" tools:context="com.example.wechat.MainActivity" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/viewpager_main" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"></android.support.v4.view.ViewPager> <com.example.sdk.widgets.ColorfulRadioGroup android:id="@+id/id_toolbar" android:layout_width="match_parent" android:layout_height="@dimen/navigation_bar_height" android:orientation="horizontal" app:checkedButton="@+id/id_indicator_wechat" > <com.example.sdk.widgets.ColorfulRadioButton android:id="@+id/id_indicator_wechat" android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:padding="5dp" app:icon="@drawable/icon_chat" app:text="@string/navi_wechat" app:text_size="12sp" app:color="#ff45c01a" /> <com.example.sdk.widgets.ColorfulRadioButton android:id="@+id/id_indicator_contact" android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:padding="5dp" app:icon="@drawable/icon_contact" app:text="@string/navi_contact" app:text_size="12sp" app:color="#ff45c01a" /> <com.example.sdk.widgets.ColorfulRadioButton android:id="@+id/id_indicator_discover" android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:padding="5dp" app:icon="@drawable/icon_discover" app:text="@string/navi_discover" app:text_size="12sp" app:color="#ff45c01a" /> <com.example.sdk.widgets.ColorfulRadioButton android:id="@+id/id_indicator_me" android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:padding="5dp" app:icon="@drawable/icon_me" app:text="@string/navi_me" app:text_size="12sp" app:color="#ff45c01a" /> </com.example.sdk.widgets.ColorfulRadioGroup></LinearLayout>
几个回调接口
MainFragmentPagerAdapter.java
/** * 在ViewPager中添加Fragment */public class MainFragmentPagerAdapter extends FragmentPagerAdapter { List<Fragment> mFragments; public MainFragmentPagerAdapter(FragmentManager fm) { super(fm); mFragments = new ArrayList<>(); init(); } private void init() { mFragments.add(new WechatFragment()); mFragments.add(new ContactFragment()); mFragments.add(new DiscoverFragment()); mFragments.add(new MeFragment()); } @Override public Fragment getItem(int position) { return mFragments.get(position); } @Override public int getCount() { return mFragments.size(); }}
MainPagerStateChangeListener.java
/** * 为ViewPager设置回调,在ViewPager进行滑动切换时为ColorfulRadiogroup设置渐变效果 */public class MainPagerStateChangeListener implements ViewPager.OnPageChangeListener { private ColorfulRadioGroup mNaviBar; public MainPagerStateChangeListener(ColorfulRadioGroup naviBar){ this.mNaviBar = naviBar; } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (positionOffset == 0){ return; } mNaviBar.setAlphaOffset(position,positionOffset); Log.i("tag","OnPageScrolled:" + position + positionOffset); } @Override public void onPageSelected(int position) { if (position != mNaviBar.getCheckedPosition()) mNaviBar.setCheckedPosition(position); } @Override public void onPageScrollStateChanged(int state) { }}
MainOnNavitabSelectListener
/** * 为ColorfulRadioGroup 设置回调,当某个Button被选中时,ViewPager切换到相应的Fragment */public class MainOnNavitabSelectListner implements OnCheckedChangeListener { private ViewPager mViewPager; public MainOnNavitabSelectListner(ViewPager viewPager){ this.mViewPager = viewPager; } @Override public void onCheckedChanged(ColorfulRadioGroup group, int position) { mViewPager.setCurrentItem(position,false); }}
MainActivity.java
package com.example.wechat;import android.os.Bundle;import android.support.v4.view.ViewPager;import android.support.v7.app.AppCompatActivity;import android.view.Menu;import android.view.View;import android.widget.RadioGroup;import com.example.sdk.widgets.ColorfulRadioGroup;import com.example.wechat.base.BaseActivity;import com.example.wechat.components.MainFragmentPagerAdapter;import com.example.wechat.components.MainOnNavitabSelectListner;import com.example.wechat.components.MainPagerStateChangeListener;public class MainActivity extends BaseActivity { private ColorfulRadioGroup mToolbar; private ViewPager mViewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { mToolbar = (ColorfulRadioGroup) findViewById(R.id.id_toolbar); mViewPager = (ViewPager) findViewById(R.id.viewpager_main); mViewPager.setAdapter(new MainFragmentPagerAdapter(getSupportFragmentManager())); mViewPager.addOnPageChangeListener(new MainPagerStateChangeListener(mToolbar)); mToolbar.setOnCheckedChangeListener(new MainOnNavitabSelectListner(mViewPager)); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return super.onCreateOptionsMenu(menu); }}
最后分享一下自己制作的icon资源
微信底部导航栏icon
附项目github地址,欢迎学习交流
github项目地址
- 高仿微信app实战(二)- 自定义View实现底部导航栏
- 底部导航栏(自定义View+ViewPager实现) android项目详解
- “fullLoad” app(二)之底部导航功能实现
- Android底部导航栏实现(二)之RadioGroup
- android仿微信底部导航栏+viewPager+自定义view
- # 仿QQ底部导航栏的自定义view
- Android开发实战2----圆点导航指示器(使用自定义View实现)
- Android App应用底部导航栏实现的一种方式
- Android App底部导航栏的另一种实现方式
- 项目之底部导航栏(二)
- 底部导航栏实现
- Android 通讯录(2)-----自定义View实现右侧导航栏
- 初学自定义view实现viewpager导航栏
- 【Android】安卓开发实战之使用Fragment(碎片)实现底部导航栏效果
- TabActivity自定义底部导航栏
- 自定义底部导航栏图标
- 开发微信小程序(3)-全局配置app.json及底部导航栏实现
- 带有指示器的自定义底部导航栏的实现
- &a[0]和&a 的区别?
- NGUI 中 Label 字体大小变化的问题
- Cookie学习
- 【Linux】进度条
- 计算机网络面试常考知识点
- 高仿微信app实战(二)- 自定义View实现底部导航栏
- Android学习之基于DrawerLayout的侧边栏实现
- apache静态服务器配置
- 搭建IntelliJ IDEA+gitbucket+git多人开发环境
- Spring MVC Hello案例
- 最流行的Java IDE有哪些?
- 关于MFC中通过向导添加变量的问题
- Windows server 2008下配置tomcat到系统服务方法及一般问题解决办法
- 如何简单使用ngrok,将网站内网映射到外网