当前位置:首页 > 技术 > 正文内容

基于Fragment与ViewPager实现微信风格页面切换

访客 技术 2026年6月26日 1

在Android开发中,实现类似微信5.0的滑动切换界面,常被误认为依赖TabHost,实则核心是通过Fragment配合ViewPager完成。这种设计不仅提升了用户体验,也体现了对UI交互逻辑的深入理解。本文将详细解析如何使用Fragment与ViewPager构建一个可动态切换、支持颜色渐变和文本高亮的底部导航栏。

最终效果如下所示:

微信风格页面切换效果

一、自定义控件:带颜色过渡的图标按钮

我们首先创建一个自定义视图,用于呈现底部导航图标,并支持在滑动过程中实现颜色渐变与文字叠加效果。

public class DynamicIconWithText extends View {

    private Bitmap iconBitmap;
    private Paint paint;
    private Rect iconRect;
    private String label = "首页";
    private int textColor = 0xFF555555;
    private int highlightColor = 0xFF45C01A;
    private float alpha = 0f;
    private Paint textPaint;
    private Rect textBounds = new Rect();

    public DynamicIconWithText(Context context) {
        super(context);
        init();
    }

    public DynamicIconWithText(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                R.styleable.DynamicIconView, 0, 0);

        try {
            iconBitmap = ((BitmapDrawable) a.getDrawable(R.styleable.DynamicIconView_icon)).getBitmap();
            highlightColor = a.getColor(R.styleable.DynamicIconView_highlight_color, 0xFF45C01A);
            label = a.getString(R.styleable.DynamicIconView_label);
            int textSize = a.getDimensionPixelSize(R.styleable.DynamicIconView_text_size, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, getResources().getDisplayMetrics()));
            textPaint.setTextSize(textSize);
        } finally {
            a.recycle();
        }
        init();
    }

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(textColor);
        textPaint.getTextBounds(label, 0, label.length(), textBounds);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int size = Math.min(getMeasuredWidth(), getMeasuredHeight());
        int iconSize = Math.min(size - getPaddingLeft() - getPaddingRight(),
                size - getPaddingTop() - getPaddingBottom() - textBounds.height());

        int left = (getMeasuredWidth() - iconSize) / 2;
        int top = (getMeasuredHeight() - textBounds.height() - iconSize) / 2;
        iconRect = new Rect(left, top, left + iconSize, top + iconSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int alphaValue = (int) (255 * alpha);
        canvas.drawBitmap(iconBitmap, null, iconRect, null);

        // 绘制高亮层
        Bitmap overlay = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas overlayCanvas = new Canvas(overlay);
        paint.setColor(highlightColor);
        paint.setAlpha(alphaValue);
        overlayCanvas.drawRect(iconRect, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        paint.setAlpha(255);
        overlayCanvas.drawBitmap(iconBitmap, null, iconRect, paint);
        canvas.drawBitmap(overlay, 0, 0, null);

        // 绘制原始文字(透明)
        textPaint.setColor(0x333333);
        textPaint.setAlpha(255 - alphaValue);
        canvas.drawText(label, iconRect.centerX() - textBounds.width() / 2,
                iconRect.bottom + textBounds.height(), textPaint);

        // 绘制高亮文字
        textPaint.setColor(highlightColor);
        textPaint.setAlpha(alphaValue);
        canvas.drawText(label, iconRect.centerX() - textBounds.width() / 2,
                iconRect.bottom + textBounds.height(), textPaint);
    }

    public void setHighlightAlpha(float value) {
        this.alpha = value;
        invalidate();
    }

    public void setIconResource(int resId) {
        this.iconBitmap = BitmapFactory.decodeResource(getResources(), resId);
        invalidate();
    }

    public void setHighlightColor(int color) {
        this.highlightColor = color;
        invalidate();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable("super_state", super.onSaveInstanceState());
        bundle.putFloat("alpha", alpha);
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            alpha = bundle.getFloat("alpha");
            super.onRestoreInstanceState(bundle.getParcelable("super_state"));
        } else {
            super.onRestoreInstanceState(state);
        }
    }
}

二、定义自定义属性

res/values/attrs.xml 中声明自定义属性:

<resources>
    <attr name="icon" format="reference"/>
    <attr name="highlight_color" format="color"/>
    <attr name="label" format="string"/>
    <attr name="text_size" format="dimension"/>

    <declare-styleable name="DynamicIconView">
        <attr name="icon"/>
        <attr name="highlight_color"/>
        <attr name="label"/>
        <attr name="text_size"/>
    </declare-styleable>
</resources>

三、底部背景样式

res/drawable/tab_bg.xml 中定义底部栏背景:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#F7F7F7"/>
    <stroke android:width="1dp" android:color="#eee"/>
</shape>

四、字符串资源配置

res/values/strings.xml 中添加标签:

<resources>
    <string name="app_name">微信</string>
    <string name="tab_home">首页</string>
    <string name="tab_contacts">联系人</string>
    <string name="tab_discover">发现</string>
    <string name="tab_profile">我</string>
</resources>

五、主布局文件

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@drawable/tab_bg"
        android:orientation="horizontal">

        <com.example.dynamictabs.DynamicIconWithText
            android:id="@+id/indicator_home"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:padding="5dp"
            app:icon="@drawable/ic_home"
            app:label="@string/tab_home"
            app:text_size="12sp"/>

        <com.example.dynamictabs.DynamicIconWithText
            android:id="@+id/indicator_contacts"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:padding="5dp"
            app:icon="@drawable/ic_contacts"
            app:label="@string/tab_contacts"
            app:text_size="12sp"/>

        <com.example.dynamictabs.DynamicIconWithText
            android:id="@+id/indicator_discover"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:padding="5dp"
            app:icon="@drawable/ic_discover"
            app:label="@string/tab_discover"
            app:text_size="12sp"/>

        <com.example.dynamictabs.DynamicIconWithText
            android:id="@+id/indicator_profile"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:padding="5dp"
            app:icon="@drawable/ic_profile"
            app:label="@string/tab_profile"
            app:text_size="12sp"/>
    </LinearLayout>
</LinearLayout>

六、Fragment实现

每个页面由独立的Fragment承载:

public class PageFragment extends Fragment {
    private String title;

    public static PageFragment newInstance(String title) {
        PageFragment fragment = new PageFragment();
        Bundle args = new Bundle();
        args.putString("title", title);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            title = getArguments().getString("title");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        TextView textView = new TextView(getActivity());
        textView.setText(title);
        textView.setGravity(Gravity.CENTER);
        textView.setTextSize(18);
        textView.setBackgroundColor(Color.WHITE);
        return textView;
    }
}

七、主Activity逻辑

MainActivity.java

public class MainActivity extends FragmentActivity implements ViewPager.OnPageChangeListener, View.OnClickListener {

    private ViewPager viewPager;
    private List<Fragment> fragments;
    private FragmentStatePagerAdapter adapter;
    private List<DynamicIconWithText> indicators;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        viewPager = findViewById(R.id.view_pager);
        initFragments();
        viewPager.setAdapter(adapter);
        viewPager.addOnPageChangeListener(this);

        initIndicators();
    }

    private void initFragments() {
        fragments = new ArrayList<>();
        String[] titles = {"首页", "联系人", "发现", "我的"};
        for (String title : titles) {
            fragments.add(PageFragment.newInstance(title));
        }
        adapter = new FragmentStatePagerAdapter(getSupportFragmentManager()) {
            @Override
            public int getCount() {
                return fragments.size();
            }

            @Override
            public Fragment getItem(int position) {
                return fragments.get(position);
            }
        };
    }

    private void initIndicators() {
        indicators = new ArrayList<>();
        DynamicIconWithText home = findViewById(R.id.indicator_home);
        DynamicIconWithText contacts = findViewById(R.id.indicator_contacts);
        DynamicIconWithText discover = findViewById(R.id.indicator_discover);
        DynamicIconWithText profile = findViewById(R.id.indicator_profile);

        indicators.add(home);
        indicators.add(contacts);
        indicators.add(discover);
        indicators.add(profile);

        home.setOnClickListener(this);
        contacts.setOnClickListener(this);
        discover.setOnClickListener(this);
        profile.setOnClickListener(this);

        home.setHighlightAlpha(1.0f);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (positionOffset > 0) {
            DynamicIconWithText left = indicators.get(position);
            DynamicIconWithText right = indicators.get(position + 1);
            left.setHighlightAlpha(1 - positionOffset);
            right.setHighlightAlpha(positionOffset);
        }
    }

    @Override
    public void onPageSelected(int position) {
        resetAllIndicators();
        indicators.get(position).setHighlightAlpha(1.0f);
    }

    @Override
    public void onPageScrollStateChanged(int state) {}

    @Override
    public void onClick(View v) {
        resetAllIndicators();
        switch (v.getId()) {
            case R.id.indicator_home:
                viewPager.setCurrentItem(0, false);
                indicators.get(0).setHighlightAlpha(1.0f);
                break;
            case R.id.indicator_contacts:
                viewPager.setCurrentItem(1, false);
                indicators.get(1).setHighlightAlpha(1.0f);
                break;
            case R.id.indicator_discover:
                viewPager.setCurrentItem(2, false);
                indicators.get(2).setHighlightAlpha(1.0f);
                break;
            case R.id.indicator_profile:
                viewPager.setCurrentItem(3, false);
                indicators.get(3).setHighlightAlpha(1.0f);
                break;
        }
    }

    private void resetAllIndicators() {
        for (DynamicIconWithText indicator : indicators) {
            indicator.setHighlightAlpha(0f);
        }
    }
}

至此,一个具备滑动切换、动态高亮、点击响应的微信式界面已完整实现。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。