高仿微信app实战(二)- 自定义View实现底部导航栏

高仿微信app实战(二)- 自定义View实现底部导航栏

话不多说,先上效果图
高仿微信app实战(二)- 自定义View实现底部导航栏_第1张图片

本文要实现微信底部导航栏,通过自定义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中声明这些属性


<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 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 && positiontrue);
    }

    /**
     * 获得当前被选中button的位置
     * @return
     */
    public int getCheckedPosition(){
        return mCheckedPosition;
    }

    /**
     * 

Interface definition for a callback to be invoked when the checked * radio button changed in this group.

*/
public interface OnCheckedChangeListener{ /** *

Called when the checked radio button has changed. When the * selection is cleared, checkedId is -1.

* * @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

"1.0" encoding="utf-8"?>
"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">

    .support.v4.view.ViewPager
        android:id="@+id/viewpager_main"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">.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>




几个回调接口

MainFragmentPagerAdapter.java


/**
 * 在ViewPager中添加Fragment
 */

public class MainFragmentPagerAdapter extends FragmentPagerAdapter {
    List 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项目地址

你可能感兴趣的:(Android)