如何利用Flutter实现酷狗流畅Tabbar效果

目录
  • 前言
  • 效果图
  • 分析效果
  • 开发思路
  • FlutterTabbar解析源码
  • 实现步骤
  • 业务使用
  • 写在最后
  • 实现源码

前言

在2021年末,酷狗发布了最新版11.0.0版本,这是一次重大的UI重构,更新完打开着实让我耳目一新。在原有风格上,整个App变得更加清爽,流畅。其中Tabbar的风格让我非常感兴趣,如果用Flutter来实现,或许是一个很有趣的事情。

效果图

分析效果

研究酷狗Tabbar的动画可以发现,默认状态下在当前Tab的中心处展示圆点,滑动时的效果拆分成两个以下部分:

  • 从单个Tab A的中心根据X轴平移到Tab B的中心位置;
  • 指示器的长度从圆点变长,再缩短为圆点。其中最大长度是可变的,跟两个Tab的大小和距离都有关系;
  • 指示器虽然依赖Tab的size和offset来变换,但和Tab却基本是同一时间渲染的,整个过程非常顺滑;
  • 总的来说,酷狗的效果就是改变了指示器的渲染动画而已。

开发思路

从上面的分析可以明确,指示器的滑动效果一定跟每个Tab的size和offset相关。那在Flutter中,获取渲染信息我们马上能想到GlobalKey,通过GlobalKey的currentContext对象获取Rander信息,但这必须在视图渲染完成后才能获取,也就是说Tab渲染完才能开始计算并渲染指示器。很显然不符合体验要求,同时频繁使用GlobalKey也会导致性能较差。

转变思路,我们需要在Tab渲染的不断把信息传给指示器,然后更新指示器,这种方式自然想到了CustomPainter【之前写了很多Canvas的控件,都是根据传入的值进行绘制,从而实现控件的变化了layout类】。在Tab updateWidget的时候,不断把Rander的信息传给画笔Painter,然后更新绘制,理论上这样做是完全行得通的。

Flutter Tabbar 解析源码

为了验证我的思路,我开始研究官方Tabbar是如何写的:

  • 进入TabBar类,直接查看build方法,可以看到为每个Tab加入了Globalkey,然后指示器用CustomPaint进行绘制;
Widget build(BuildContext context) {

  // ...此处省略部分代码...

  final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
    const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
    EdgeInsetsGeometry? adjustedPadding;
    // 这里为tab加入Globalkey,以便后续获取Tab的渲染信息
    if (widget.tabs[index] is PreferredSizeWidget) {
      final PreferredSizeWidget tab = widget.tabs[index] as PreferredSizeWidget;
      if (widget.tabHasTextAndIcon && tab.preferredSize.height == _kTabHeight) {
        if (widget.labelPadding != null || tabBarTheme.labelPadding != null) {
          adjustedPadding = (widget.labelPadding ?? tabBarTheme.labelPadding!).add(const EdgeInsets.symmetric(vertical: verticalAdjustment));
        }
        else {
          adjustedPadding = const EdgeInsets.symmetric(vertical: verticalAdjustment, horizontal: 16.0);
        }
      }
    }

    // ...此处省略部分代码...

    // 可以看到指示器是CustomPaint对象
    Widget tabBar = CustomPaint(
        painter: _indicatorPainter,
        child: _TabStyle(
            animation: kAlwaysDismissedAnimation,
            selected: false,
            labelColor: widget.labelColor,
            unselectedLabelColor: widget.unselectedLabelColor,
            labelStyle: widget.labelStyle,
            unselectedLabelStyle: widget.unselectedLabelStyle,
            child: _TabLabelBar(
              onPerformLayout: _saveTabOffsets,
              children: wrappedTabs,
        ),
      ),
    );
  • 绘制指示器用CustomPaint跟我们的预想一致,那如何把绘制的size和offset传进去呢。我们来看_TabLabelBar继承于Flex,而Flex又继承自MultiChildRenderObjectWidget,重写其createRenderObject方法;
class _TabLabelBar extends Flex {
  _TabLabelBar({
    Key? key,
    List<Widget> children = const <Widget>[],
    required this.onPerformLayout,
  }) : super(
    key: key,
    children: children,
    direction: Axis.horizontal,
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.center,
    verticalDirection: VerticalDirection.down,
  );

  final _LayoutCallback onPerformLayout;

  @override
  RenderFlex createRenderObject(BuildContext context) {
    // 查看下_TabLabelBarRenderer
    return _TabLabelBarRenderer(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
      textDirection: getEffectiveTextDirection(context)!,
      verticalDirection: verticalDirection,
      onPerformLayout: onPerformLayout,
    );
  }

  @override
  void updateRenderObject(BuildContext context, _TabLabelBarRenderer renderObject) {
    super.updateRenderObject(context, renderObject);
    renderObject.onPerformLayout = onPerformLayout;
  }
}

查看真实的渲染对象:_TabLabelBarRenderer,在performLayout中返回渲染的size和offset,并通过TabBar传入的_saveTabOffsets方法保存到_indicatorPainter中;_saveTabOffsets尤为重要,把Tabbar的渲染位移通知给Painter,从而让Painter可以轻松算出tab之间的宽度差

class _TabLabelBarRenderer extends RenderFlex {
  _TabLabelBarRenderer({
    List<RenderBox>? children,
    required Axis direction,
    required MainAxisSize mainAxisSize,
    required MainAxisAlignment mainAxisAlignment,
    required CrossAxisAlignment crossAxisAlignment,
    required TextDirection textDirection,
    required VerticalDirection verticalDirection,
    required this.onPerformLayout,
  }) : assert(onPerformLayout != null),
       assert(textDirection != null),
       super(
         children: children,
         direction: direction,
         mainAxisSize: mainAxisSize,
         mainAxisAlignment: mainAxisAlignment,
         crossAxisAlignment: crossAxisAlignment,
         textDirection: textDirection,
         verticalDirection: verticalDirection,
       );

  _LayoutCallback onPerformLayout;

  @override
  void performLayout() {
    super.performLayout();
    // xOffsets will contain childCount+1 values, giving the offsets of the
    // leading edge of the first tab as the first value, of the leading edge of
    // the each subsequent tab as each subsequent value, and of the trailing
    // edge of the last tab as the last value.
    RenderBox? child = firstChild;
    final List<double> xOffsets = <double>[];
    while (child != null) {
      final FlexParentData childParentData = child.parentData! as FlexParentData;
      xOffsets.add(childParentData.offset.dx);
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
    }
    assert(textDirection != null);
    switch (textDirection!) {
      case TextDirection.rtl:
        xOffsets.insert(0, size.width);
        break;
      case TextDirection.ltr:
        xOffsets.add(size.width);
        break;
    }
    onPerformLayout(xOffsets, textDirection!, size.width);
  }
}
  • 通过Tabbar中的didChangeDependencies和didUpdateWidget生命周期,更新指示器;
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  assert(debugCheckHasMaterial(context));
  final TabBarTheme tabBarTheme = TabBarTheme.of(context);
  _updateTabController();
  _initIndicatorPainter(adjustedPadding, tabBarTheme);
}

@override
void didUpdateWidget(KuGouTabBar oldWidget) {
  super.didUpdateWidget(oldWidget);
  final TabBarTheme tabBarTheme = TabBarTheme.of(context);
  if (widget.controller != oldWidget.controller) {
    _updateTabController();
    _initIndicatorPainter(adjustedPadding, tabBarTheme);
  } else if (widget.indicatorColor != oldWidget.indicatorColor ||
      widget.indicatorWeight != oldWidget.indicatorWeight ||
      widget.indicatorSize != oldWidget.indicatorSize ||
      widget.indicator != oldWidget.indicator) {
    _initIndicatorPainter(adjustedPadding, tabBarTheme);
  }

  if (widget.tabs.length > oldWidget.tabs.length) {
    final int delta = widget.tabs.length - oldWidget.tabs.length;
    _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
  } else if (widget.tabs.length < oldWidget.tabs.length) {
    _tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length);
  }
}
  • 然后重点就在指示器_IndicatorPainter如何进行绘制了。

实现步骤

通过理解Flutter Tabbar的实现思路,大体跟我们预想的差不多。不过官方继承了Flex来计算Offset和size,实现起来很优雅。所以我也不班门弄斧了,直接改动官方的Tabbar就可以了。

  • 创建KuGouTabbar,复制官方代码,修改引用,删除无关的类,只保留Tabbar相关的代码。

2. 重点修改_IndicatorPainter,根据我们的需求来绘制指示器。在painter方法中,我们可以通过controller拿到当前tab的index以及animation!.value, 我们模拟下切换的过程,当tab从第0个移到第1个,动画的值从0变成1,然后动画走到0.5时,tab的index会从0突然变为1,指示器应该是先变长,然后在动画走到0.5时,再变短。因此动画0.5之前,我们用动画的value-index作为指示器缩放的倍数,指示器不断增大;动画0.5之后,用index-value作为缩放倍数,不断缩小。

final double index = controller.index.toDouble();

final double value = controller.animation!.value;
/// 改动 ltr为false,表示索引还是0,动画执行未超过50%;ltr为true,表示索引变为1,动画执行超过50%
final bool ltr = index > value;
final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex);
final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex);

/// 改动 通过ltr来决定是放大还是缩小倍数,可以得出公式:ltr ? (index - value) : (value - index)
final Rect fromRect =
    indicatorRect(size, from, ltr ? (index - value) : (value - index));

/// 改动
final Rect toRect =
    indicatorRect(size, to, ltr ? (index - value) : (value - index));
_currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());

而指示器接收缩放倍数的前提还需要计算指示器最大的宽度,并且上面是根据动画的0.5作为最大的宽度,也就是移动到一半的时候,指示器应该达到最大宽度。因此指示器最大的宽度是需要️2的。请看下面代码:

class _IndicatorPainter extends CustomPainter {
  ......此处省略部分代码......

  void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
    _currentTabOffsets = tabOffsets;
    _currentTextDirection = textDirection;
  }

  // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
  // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
  int get maxTabIndex => _currentTabOffsets!.length - 2;

  double centerOf(int tabIndex) {
    assert(_currentTabOffsets != null);
    assert(_currentTabOffsets!.isNotEmpty);
    assert(tabIndex >= 0);
    assert(tabIndex <= maxTabIndex);
    return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
        2.0;
  }

  /// 接收上面代码分析中传入的倍数 scale
  Rect indicatorRect(Size tabBarSize, int tabIndex, double scale) {
    assert(_currentTabOffsets != null);
    assert(_currentTextDirection != null);
    assert(_currentTabOffsets!.isNotEmpty);
    assert(tabIndex >= 0);
    assert(tabIndex <= maxTabIndex);
    double tabLeft, tabRight, tabWidth = 0;
    switch (_currentTextDirection!) {
      case TextDirection.rtl:
        tabLeft = _currentTabOffsets![tabIndex + 1];
        tabRight = _currentTabOffsets![tabIndex];
        break;
      case TextDirection.ltr:
        tabLeft = _currentTabOffsets![tabIndex];
        tabRight = _currentTabOffsets![tabIndex + 1];
        break;
    }

    /// 改动,通过GlobalKey计算出渲染的文本的宽度
    tabWidth = tabKeys[tabIndex].currentContext!.size!.width;
    final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
    tabLeft += delta;
    tabRight -= delta;

    final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection);

    /// 改动,算出指示器的最大宽度,记得*2
    double maxLen = (tabRight - tabLeft + insets.horizontal) * 2;

    double res =
        scale == 0 ? minWidth : maxLen * (scale < 0.5 ? scale : 1 - scale);

    /// 改动
    final Rect rect = Rect.fromLTWH(tabLeft + tabWidth / 2 - minWidth / 2, 0.0, res > minWidth ? res : minWidth, tabBarSize.height);

    if (!(rect.size >= insets.collapsedSize)) {
      throw FlutterError(
        'indicatorPadding insets should be less than Tab Size\n'
        'Rect Size : ${rect.size}, Insets: ${insets.toString()}',
      );
    }
    return insets.deflateRect(rect);
   }
}
  • 如上,指示器的宽度我们根据controller切换时的index和动画值进行转化,实现宽度的变化。而Offset的最小值和最大值分别是切换前后两个Tab的中心点,这里应该做下相应的的限制,然后传给Rect.fromLTWH。

【由于时间和精力问题,我并没有去做这一步的实现,而且酷狗那边动画跟滑动逻辑的关系需要UI给出具体的公式,才能百分百还原。】

最后就是加多一个参数,让业务方传入指示器的最小宽度。

/// 指示器的最小宽度
final double indicatorMinWidth;

业务使用

在上面我们已经把简单的动画效果改完了,接下来就是传入圆角的indicator、最小宽度indicatorMinWidth,就可以正常使用啦。

  • 圆角的指示器,我直接上源码
import 'package:flutter/material.dart';

class RRecTabIndicator extends Decoration {
  const RRecTabIndicator(
      {this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
        this.insets = EdgeInsets.zero,
        this.radius = 0,
        this.color = Colors.white});

  final double radius;
  final Color color;
  final BorderSide borderSide;
  final EdgeInsetsGeometry insets;

  @override
  Decoration? lerpFrom(Decoration? a, double t) {
    if (a is RRecTabIndicator) {
      return RRecTabIndicator(
        borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
        insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  Decoration? lerpTo(Decoration? b, double t) {
    if (b is RRecTabIndicator) {
      return RRecTabIndicator(
        borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
        insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  _UnderlinePainter createBoxPainter([VoidCallback? onChanged]) {
    return _UnderlinePainter(this, onChanged);
  }

  Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
    final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
    return Rect.fromLTWH(
      indicator.left,
      indicator.bottom - borderSide.width,
      indicator.width,
      borderSide.width,
    );
  }

  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return Path()..addRect(_indicatorRectFor(rect, textDirection));
  }
}

class _UnderlinePainter extends BoxPainter {
  _UnderlinePainter(this.decoration, VoidCallback? onChanged)
      : super(onChanged);

  final RRecTabIndicator decoration;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final Rect rect = offset & configuration.size!;
    final TextDirection textDirection = configuration.textDirection!;
    final Rect indicator = decoration._indicatorRectFor(rect, textDirection);
    final Paint paint = decoration.borderSide.toPaint()
      ..strokeCap = StrokeCap.square
      ..color = decoration.color;
    final RRect rRect =
    RRect.fromRectAndRadius(indicator, Radius.circular(decoration.radius));
    canvas.drawRRect(rRect, paint);
  }
}
  • 调用非常简单,跟原来官方代码一模一样。
Scaffold(
  appBar: AppBar(
    // Here we take the value from the MyHomePage object that was created by
    // the App.build method, and use it to set our appbar title.
    title: Text(widget.title),
    bottom: KuGouTabBar(
      tabs: const [Tab(text: "音乐"), Tab(text: "动态"), Tab(text: "语文")],
      // labelPadding: EdgeInsets.symmetric(horizontal: 8),
      controller: _tabController,
      // indicatorSize: TabBarIndicatorSize.label,
      // isScrollable: true,
      padding: EdgeInsets.zero,
      indicator: const RRecTabIndicator(
          radius: 4, insets: EdgeInsets.only(bottom: 5)),
      indicatorMinWidth: 6,
    ),
  ),
);  

写在最后

模仿酷狗的Tabbar效果,就分享到这里啦,重点在于实现步骤的第2、3步,涉及到一些简单的数学知识。说说心得吧,Flutter UI层面的问题,其实技术栈已经很单一了。只要跟着官方的实现思路,能写出跟其类似的代码,把Rander层理解透彻,笔者认为已经足够了。往深了还是得往原生、混编、解决Flutter痛点问题为主。 希望一起共勉!!!

实现源码

https://github.com/WxqKb/KuGouTabbar.git

到此这篇关于如何利用Flutter实现酷狗流畅Tabbar效果的文章就介绍到这了,更多相关Flutter实现酷狗Tabbar效果内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Flutter 滚动监听及实战appBar滚动渐变的实现

    介绍 在 Flutter 中滚动监听一般可以采用两种方式来实现,分别是ScrollController和NotificationListener这两种方式. ScrollController介绍 ScrollController 介绍一下ScrollController常用的属性和方法: offset:可滚动组件当前的滚动位置. jumpTo(double offset)跳转到指定位置,offset为滚动偏移量. animateTo(double offset,@required Duratio

  • Flutter沉浸式状态栏/AppBar导航栏/仿咸鱼底部凸起导航栏效果

    如下图:状态栏是指android手机顶部显示手机状态信息的位置. android 自4.4开始新加入透明状态栏功能,状态栏可以自定义颜色背景,使titleBar能够和状态栏融为一体,增加沉浸感. 如上图Flutter状态栏默认为黑色半透明,那么如何去掉这个状态栏的黑色半透明背景色,让其和标题栏颜色一致,通栏沉浸式,实现如下图效果呢?且继续看下文讲述. 在flutter项目目录下找到android主入口页面MainActivity.kt或MainActivity.java,判断一下版本号然后将状态

  • flutter实现appbar下选项卡切换

    本文实例为大家分享了flutter实现appbar下选项卡切换的具体代码,供大家参考,具体内容如下 TabBar .Tab.TabBarView 结合实现 这里实现的是appbar下的选项卡 import 'package:flutter/material.dart'; /** * 有状态StatefulWidget * 继承于 StatefulWidget,通过 State 的 build 方法去构建控件 */ class TabBarAndTopTab extends StatefulWid

  • Flutter自定义Appbar搜索框效果

    本文实例为大家分享了Flutter自定义Appbar搜索框效果的具体代码,供大家参考,具体内容如下 首先看一下实现本次实现的效果. 本来想直接偷懒从flutter pub上找个能用的使用起来,但是看了下发现都与目前ui效果相差很大,于是乎决定自己实现一个.整体的话比较简单,本来也是为了练手而做的. 为了方便处理statusbar的高度适配,于是乎直接依托于Appbar进行了实现,这样就可以不用处理状态栏适配了. class _HotWidgetState extends State<HotWid

  • 如何利用Flutter实现酷狗流畅Tabbar效果

    目录 前言 效果图 分析效果 开发思路 FlutterTabbar解析源码 实现步骤 业务使用 写在最后 实现源码 前言 在2021年末,酷狗发布了最新版11.0.0版本,这是一次重大的UI重构,更新完打开着实让我耳目一新.在原有风格上,整个App变得更加清爽,流畅.其中Tabbar的风格让我非常感兴趣,如果用Flutter来实现,或许是一个很有趣的事情. 效果图 分析效果 研究酷狗Tabbar的动画可以发现,默认状态下在当前Tab的中心处展示圆点,滑动时的效果拆分成两个以下部分: 从单个Tab

  • Flutter 实现酷炫的3D效果示例代码

    此文讲解3个酷炫的3D动画效果. 下面是要实现的效果: Flutter 中3D效果是通过 Transform 组件实现的,没有变换效果的实现: class TransformDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('3D 变换Demo'), ), body: Container( alignm

  • 利用Flutter实现“孔雀开屏”的动画效果

    前言 今天分享一个类似"孔雀开屏"的动画效果,打开新的页面时,新的页面从屏幕右上角以圆形逐渐打开到全屏. 先来看下具体的效果 不知道这种效果大家叫什么名字?如果有更合适的名字可以在评论处告诉我,下面来说下如何实现此效果. 在使用Navigator进入一个新的页面时,通常用法如下: Navigator.of(context).push(MaterialPageRoute( builder: (context){ return PageB(); } )); MaterialPageRout

  • 如何利用Flutter仿写微信搜索页效果

    目录 效果图 顶部搜索栏 SearchBar 实现细节 左边搜索框实现 右边取消按钮实现 内容的检索 内容的传递 内容的检索 搜索列表实现 总结 效果图 如上图所示,我们用 Flutter 来仿写搜索页面,这里聊天首页点击搜索栏会跳转到搜索页,搜索页面包含顶部搜索框跟底部 ListView,在搜索框内我们输入搜索词会检索聊天列表模型中 name 属性中包含搜索词的模型,并在底部列表中展示,且搜索词高亮显示.下面我们分别来介绍下这些功能的实现. 顶部搜索栏 class SearchBar exte

  • python获取酷狗音乐top500的下载地址 MP3格式

    下面先给大家介绍下python获取酷狗音乐top500的下载地址 MP3格式,具体代码如下所示: # -*- coding: utf-8 -*- # @Time : 2018/4/16 # @File : kugou_top500.py # @Software: PyCharm # @pyVer : python 2.7 import requests,json headers={ 'UserAgent' : 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 lik

  • java爬取并下载酷狗TOP500歌曲的方法

    是这样的,之前买车送的垃圾记录仪不能用了,这两天狠心买了好点的记录仪,带导航.音乐.蓝牙.4G等功能,寻思,既然有这些功能就利用起来,用4G听歌有点奢侈,就准备去酷狗下点歌听,居然都是需要办会员才能下载,而且vip一月只能下载300首,我这么穷又这么抠怎么可能冲会员,于是百度搜了下怎么免费下载,都是python爬取,虽然也会一点,但是电脑上没安装python,再安装再研究感觉有点费劲,于是就花了半小时做了这个爬虫,技术一般,只记录分析实现过程,大牛请绕行.其中用到了一些库,包括:jsoup.Ht

  • 将酷狗krc歌词解析并转换为lrc歌词php源码

    最近在进行一次对酷狗音乐歌词采集时发现酷狗音乐的歌词直接浏览都是"乱码",自己平时所见的歌词都是lrc格式的文本,这种酷狗专用的krc格式的显然是经过特别处理过的,平时用酷狗听音乐也没仔细看他的歌词有什么不同,只是与天天静听等不同的是可以逐字高亮显示歌词. 对酷狗的flash播放器进行反编译,发现这段krc解密的ActionScript代码: public function loaderCompleteHandler(param1:ByteArray) : void{ this.new

  • JS模拟酷狗音乐播放器收缩折叠关闭效果代码

    本文实例讲述了JS模拟酷狗音乐播放器收缩折叠关闭效果代码.分享给大家供大家参考,具体如下: 这是一款模拟酷狗音乐播放器的关闭特效,采用JavaScript实现,关闭的时候播放界面缩成一条线,然后消失,就像有些电视机突然停电的效果,很有意思的网页动画特效. 运行效果截图如下: 在线演示地址如下: http://demo.jb51.net/js/2015/js-kugou-music-player-style-demo/ 具体代码如下: <!DOCTYPE html PUBLIC "-//W3

  • 酷狗去广告一键去除批处理代码

    酷狗去广告的原理大家可以网上找找,都大同小异,就是对如下几个配置文件下手 game.ini game.inicfg optionv5.ini optionv5.inicfg 我是狠了点,如下操作: 1.直接将这几个配置文件给删 了,然后创建与配置文件同名文件夹 2.再在新创建的配置文件同名文件夹下创建CON子文件夹,此步骤可保证去广告效果不反弹 3.删除名为"AD"的文件夹,创建一个同名的空白文件,设置其文档属性为只读 ====已去广告,若要视觉清爽,可将以上新建的文件或文件夹设为隐藏

  • Java swing仿酷狗音乐播放器

    今天给大家介绍下用Java swing开发一款音乐播放器,高仿酷狗音乐播放器,完整源码地址在最下方,本文只列出部分源码,因为源码很多,全部贴不下,下面还是老规矩.来看看运行结果: 下面我们来看看代码: 首先看一下主窗口的实现代码: package com.baiting; import java.awt.Dimension; import java.awt.Toolkit; import com.baiting.menu.CloseWindow; /** * 窗口 * @author lmq *

随机推荐