分类 技术 下的文章

最近遇到一个问题,需要判断视频文件是否是真正的视频文件。
什么意思呢?萤石的摄像头是将视频写入 TF 卡的:

通过萤石云视频平台将TF卡格式化后,程序会采用预占空间的方式预先将1/4的空间作为视频或者图片的存储空间。

然后他预写入的文件是.mp4后缀的,但是是不可播放的文件。所以一旦播放器播放它,可能就会出错了。为了避免这样的情况发生,我们能否在检索视频的时候就识别出无法播放的视频呢?

我一开始的思路是,能否通过判断文件类型的方式来判断是否播放呢?有可能他预格式化的视频文件,虽然后缀是.mp4,但是本质上不是一个视频文件呢?

判断文件类型

在 Java 中,比较常见的用来判断文件类型的库,就是Apache Tika
引入依赖后,使用起来也十分简单:

File file = new File(System.getProperty("user.home")+ "/Downloads/");
        if (file.exists()){
            for (File file1 : Objects.requireNonNull(file.listFiles())){
                if (file1.isDirectory()){
                    continue;
                }
                try {
                    String type = new Tika().detect(file1);
                    System.out.println(file1.getName() + " ======>>> " + type);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

下面是输出结果:

background.svg ======>>> image/svg+xml
new.json ======>>> application/json
mid_top_1.jpg ======>>> image/jpeg
.DS_Store ======>>> application/octet-stream
apache-maven-3.5.4-bin.tar.1.gz ======>>> application/x-gzip
这本来是张 png 图片.mp4 ======>>> image/png
.localized ======>>> application/octet-stream
old.json ======>>> application/json
不学无数 — Java 中 IO 和 NIO - 掘金.pdf ======>>> application/pdf
Layout_Mobile_Whiteframe.ai ======>>> application/pdf
Snipaste_2018-09-22_22-36-21.png ======>>> image/png
background.png ======>>> image/png
hiv00014.mp4 ======>>> video/mp4
SeimiCrawler-master.1.zip ======>>> application/zip
cn_windows_10_business_editions_version_1803_updated_march_2018_x64_dvd_12063730.iso ======>>> application/x-iso9660-image
i_con_permission.png ======>>> image/png
错误.png ======>>> image/png
open_gapps-arm64-9.0-pico-20180923.zip ======>>> application/zip
forPush.sh ======>>> application/x-sh
adbIn.sh ======>>> application/x-sh
Havoc-OS-v2.0-20180923-oneplus3-Official.zip ======>>> application/zip
MockingBot.dmg ======>>> application/octet-stream

可以看到,几乎全部格式都识别出来了。而且我这里有一个『浑水摸鱼』的『家伙』,那就是这本来是张 png 图片.mp4,人如其名。
这个也识别出来了(octet-stream指的是二进制文件)。
那 Tika 究竟是怎么做的呢?让我们来看看源码呗。
篇幅所限,如果你对这个部分有兴趣,可以移步我的另一篇文章Tika 源码浅析

这里假设你已经看过了 Tika 源码...

现在我们知道了 Tika 通过文件的首部字节、文件后缀判断文件的类型。但是这样依旧无法判断视频是否可以播放。
因为在实际使用中,我发现,有一些个视频文件,虽然是无法播放的,但是它们的首部已经被写入了,成为另一个『空视频文件』。
真的蛋疼。如果单纯使用 Tika 的话,显然会有误差。
那么有没有其他应用层面的 trick 呢?

答案就是 FFmpegMediaMetadataRetriever

FFmpegMediaMetadataRetriever 是什么?

之所以说是 trick...是因为 FFmpegMediaMetadataRetriever 是一个获取媒体文件信息的库。
我在使用时发现,如果一个视频只被写了头部,但是没有实际内容的话,该视频是没有编码信息的。
这个想法是来自MediaInfo这个软件。因为我是先用这个软件测试了一遍,发现是可行的。
然后我找到了在 Android 平台可用的一个类似的库,也就是 FFmpegMediaMetadataRetriever

使用

我们引入依赖

implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever:1.0.14'

然后开始使用:

public static List<String> getAvailableVideoList(String SDCardPath){
    if (TextUtils.isEmpty(SDCardPath)){
        return null;
    }

    List<String> pathList = new ArrayList<>();
    FFmpegMediaMetadataRetriever mediaMetadataRetriever = new FFmpegMediaMetadataRetriever();
    File fileList = new File(SDCardPath);
    if (fileList.exists()) {
        try {
            for (File file : fileList.listFiles()) {
                if (file.getName().endsWith("mp4")){
                    mediaMetadataRetriever.setDataSource(file.getPath());
                    mediaMetadataRetriever.extractMetadata(METADATA_KEY_VIDEO_CODEC);
                    pathList.add(file.getPath());
                }
            }
        } catch (IllegalArgumentException iae) {
            iae.printStackTrace();
        }
    }
    return pathList;
}

如果视频文件无效的,FFmpegMediaMetadataRetriever 会抛出一个异常:

java.lang.IllegalArgumentException: setDataSource failed: status = 0xFFFFFFFF

然后我们捕获这个异常,做一些其他的工作就可以。
这个方法就目前的使用来看,是最稳定和准确的方法。
缺点是:

  • setDataSource()过程有一些耗时,实际上测试,256MB 的视频文件,调用一次需要将近 1s 的时间
  • 会抛异常...

还有其他办法吗?

最一开始想的,其实是开一个 VideoPlayer,然后在onError()设一个监听器,如果播放错误就说明该文件无效。
但是经过我自己的评估,这样的性能实际上更差。

也不知道是否有其他更优的方法,如果有的话,还请不吝赐教~


参看

问题是什么?

如果你不具备路由器代理的情况,那么我们需要在本地做透明代理。这样的话,对本地代理的使用情况完全取决于第三方程序的支持情况。
有一些程序会自动检测代理,有一些提供配置选项。对于两种而言,设置代理都很方便。
那么还有一些 GUI 软件,但是并不提供代理设置选项,或者在首次启动的时候就必须以代理模式允许并且还不允许设置代理(说的就是你 Dropbox)。

这个时候该怎么办呢?

我们要理解一个内容就是,对于 Linux 而言,所有程序都可以通过命令行启动的。(此处的”程序”包含二进制文件、脚本等等)

所以,我们基本思路就是:

  • 找到程序本体
  • 通过命令行 + proxychains 设置代理并启动

来吧,解决方法

找到程序的本体

用 find

在 linux 下找东西方便得很呢,打开你的 terminal:


man find

这个美妙的工具可以搜索他能搜索到的东西,要找个程序还不是分分钟的事情。
举个例子。我们要找 Dropbox 的二进制文件所在:

sudo find / -name "dropbox"

搜索结果:


/usr/share/doc/libnet-dropbox-api-perl/examples/dropbox
/usr/share/doc/dropbox
/usr/bin/dropbox

结果应该已经出来了。
一般通过 dpkg 或者软件管理安装的软件都会将目录放置在 /usr/local//usr/bin/等目录中。(那个 doc就是文档的位置啦,一般放置授权协议、更新日志之类的文档。

所以这里的 dropbox 的二进制文件就在 /usr/bin/dropbox 中咯。
执行:

proxychains /usr/bin/dropbox

就可以通过代理的方式启动了。

能不能不要这么麻烦?

啊,这样很麻烦啊,难道每次都要记住位置,敲那么长的命令才能愉快地食用吗?
当然不是啊
既然本文的标题指的是桌面程序,就说明还有更好的方法啊。

当然,对于 Dropbox 而言,你首次启动之后,就可以进去设置代理了,也就不需要使用以下方法了。
下面的方法是针对:需要代理但是没有提供代理设置的桌面程序(GUI)。

解决方法

前提是你想通过图标启动,如果你只想通过命令行启动...其实写个脚本也可以

其实思路很简单,和我之前写的这篇文章类似。
桌面应用程序,一般都带有图标啊。点击图标的时候,桌面环境就会读取该程序的desktop配置,通过该配置里的内容执行程序咯。
看看你的 /usr/share/applications,里面是不是有很多desktop配置文件呐。
找到你想启动的应用程序,都会出现类似下面的内容(只多不少,因为有些程序会对本地化作处理

[Desktop Entry]
Version=1.0
Name=Zeal
GenericName=Documentation Browser
Comment=Simple API documentation browser
Exec=/usr/bin/zeal %u
Icon=zeal
Terminal=false
Type=Application
Categories=Development;
MimeType=x-scheme-handler/dash;x-scheme-handler/dash-plugin;

看到 Exec 这一行,是不是很熟悉呀,其实这就是指向程序所在的文件夹。如果该程序加入了环境变量,那你就看不到路径咯。
我们要做的就很简单了,在原来路径或程序名前面加上 proxychains 就可以咯。或者是其他代理比如tsocks,也就会变成 Exec=proxychains /usr/bin/zeal %u
保存之后重启,点击桌面图标就会自动以代理模式启动咯。

前言

在某件机缘巧合(实际上是曲折的辛酸故事)的事情发生之后,我接到了通过 Javascript 实现一个 A* 算法任务。

讲道理我在一开始接到的时候还不知道这个是什么东西...后面阅读下面的文章之后才有所了解:

上面这篇文章是译文,原文已经 404 了,好在本文翻译的还不错。我看了这篇文章才了解了这些东西。

本文章不鹦鹉学舌,误导读者。所以不会再赘述算法的流程,诸君看上述版本即可~

再接着就是实现:

我的 js 实现也基本参考他的做的。当然,我是在 Cocos Creator 上搭建了场景实现了,所以其中还有相当一部分是关于 Cocos Creator 的应用。此处先按下不表。

下面推荐一个很有意思的 Github 项目,他这个实现了诸多寻找路径的算法!还有网页版!你可以看看实现过程以助于你理解算法。


实现了 js 版本,我想着用我的老本行 Java 来实现故而有了如下的代码。

注意!因为写得匆忙,所以没有写测试代码。如有错漏,请见谅!并烦请赐教~

A* 算法 Java 实现

public class AStarFinder {
    // 分别是:直行成本,斜行成本,地图宽度和地图高度
    private static final int STRAIGHT_COST = 10;
    private static final int OBLIQUE_COST = 14;
    private static final int MAP_WIDTH = 960;
    private static final int MAP_HEIGHT = 480;

  /**
     * 节点内部类,设有坐标、f, g, h 等变量
     */
    class Pos{
        private int x;
        private int y;
        private int g;
        private int h;
        private int f;
        private Pos parent;

        public Pos(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        public int getG() {
            return g;
        }

        public void setG(int g) {
            this.g = g;
        }

        public int getH() {
            return h;
        }

        public void setH(int h) {
            this.h = h;
        }

        public int getF() {
            return f;
        }

        public void setF(int f) {
            this.f = f;
        }

        public Pos getParent() {
            return parent;
        }

        public void setParent(Pos parent) {
            this.parent = parent;
        }
    }

    /**
     * 路径查找方法
     * @param startX 起始点 x 坐标
     * @param startY 起始点 y 坐标
     * @param endX  终点 x 坐标
     * @param endY  终点 y 坐标
     * @return 返回路径坐标集合
     */
    public ArrayList<Pos> searchRoad(int startX, int startY, int endX, int endY){

        // 分别是打开点列表、关闭点列表、结果点列表和障碍点列表
        ArrayList<Pos> openList = new ArrayList<>();
        ArrayList<Pos> closeList = new ArrayList<>();
        ArrayList<Pos> resultList = new ArrayList<>();
        ArrayList<Pos> barriersList = getBarriersList();
        // 结果节点的索引
        int resultIndex = -1;
        // 是否获得了结果
        boolean isGetResult = false;

        // 将当前点加入开启列表中
        openList.add(new Pos(startX, startY));

        do {
            // 先将当前节点取出并加入关闭列表之中
            Pos currentPoint = openList.get(0);
            closeList.add(currentPoint);
            // 获取周围八个点的集合,并轮询
            ArrayList<Pos> surroundPoints = getSurroundPoints(currentPoint);
            for (Pos pos : surroundPoints){
                // 是否是障碍点
                boolean isBarrier = barriersList.contains(pos);
                // 是否是关闭列表中的点
                boolean isExistList = closeList.contains(pos);
                // 是否是在地图范围内
                boolean isInMap = pos.x >= 0 && pos.x < MAP_WIDTH/2 &&
                        pos.y >= 0 && pos.y <= MAP_HEIGHT/2;

                if (!isExistList && !isBarrier && isInMap){
                    // 均是,计算 g 值
                    int g = currentPoint.g +
                            ((currentPoint.x - pos.x) * (currentPoint.y - pos.y) == 0 ? STRAIGHT_COST : OBLIQUE_COST);
                    // 如果当前点不在打开点中,那么计算 h, f 值,并加入进入
                    if (!openList.contains(pos)){
                        pos.h = Math.abs(endX - pos.x) * 10 + Math.abs(endY - pos.y) * 10;
                        pos.g = g;
                        pos.f = pos.g + pos.h;
                        pos.parent = currentPoint;
                        openList.add(pos);
                    }else {
                        // 如果在打开点列表中,那么重新计算 g 和 f,因为 g 当前位置相关
                        int index = openList.indexOf(pos);
                        if (g < openList.get(index).g){
                            openList.get(index).parent = currentPoint;
                            openList.get(index).g = g;
                            openList.get(index).f = g + openList.get(index).h;
                        }
                    }
                }
                if (openList.isEmpty()){
                    System.out.println("没有可用通路");
                    break;
                }

                // 对打开点列表进行升序排序,每次都获得第一个 f 为最小
                openList.sort(new Comparator<Pos>() {
                    @Override
                    public int compare(Pos o1, Pos o2) {
                        return Integer.compare(o1.f, o2.f);
                    }
                });
            }
            // 遍历打开点列表看结果点是否在其中
            for (Pos tmpPos : openList){
                if (tmpPos.x == endX && tmpPos.y == endY){
                    isGetResult = true;
                    resultIndex = openList.indexOf(tmpPos);
                }
            }

        }while (isGetResult);

        if (resultIndex == -1){
            // 如果索引值为 -1 ,那么说明没有结果点
            return null;
        }else {
            // 获取路径
            Pos currentPos = openList.get(resultIndex);
            do {
                resultList.add(currentPos);
                currentPos = currentPos.parent;
            }while (currentPos.x != startX || currentPos.y != startY);
        }
        return resultList;
    }

    /**
     * 获取障碍物区域坐标集合
     * @return 返回坐标集合
     */
    private ArrayList<Pos> getBarriersList(){
        // @to-do

        return new ArrayList<Pos>();
    }

    /**
     * 获取周围八个节点函数
     * @param currentPoint 当前点
     * @return  返回八个存有节点的集合
     */
    private ArrayList<Pos> getSurroundPoints(Pos currentPoint){
        int x = currentPoint.x;
        int y = currentPoint.y;

        ArrayList<Pos> surroundPoints = new ArrayList<>();
        surroundPoints.add(new Pos(x - 1, y - 1));
        surroundPoints.add(new Pos(x , y - 1));
        surroundPoints.add(new Pos(x + 1, y - 1));
        surroundPoints.add(new Pos(x + 1, y));
        surroundPoints.add(new Pos(x + 1, y + 1));
        surroundPoints.add(new Pos(x, y + 1));
        surroundPoints.add(new Pos(x - 1, y + 1));
        surroundPoints.add(new Pos(x - 1, y));

        return surroundPoints;
    }


}

本文发布于我的博客

此文章为「译文」,原文链接:http://www.mergeconflict.net/2012/05/java-threads-vs-android-asynctask-which.html

翻译已获原作者授权。水平有限,如有缺漏,恳请指正,谢谢~

前言

在 Android 开发中,有一个非常重要但是较少被讨论到的问题:UI 的响应。这个问题一部分由 Android 系统本身决定,但更多时候是还是开发者的责任。抛开其他问题而言,解决 Android 应用 UI 响应问题的关键,就是尽可能地让大部分耗时工作转移到后台执行。众所周知,将耗时任务或是 CPU 密集型任务放到后台运行的方法,基本上只有两个:

  • Java Thread
  • Android 原生AsyncTask辅助类

两者不一定能分出个孰优孰劣,因此了解他们各自的使用场景,对您的优化性能是有一定的好处的。

AsyncTask 的使用场景

  • 不需要下载大量数据的简单网络操作
  • I/O 密集型任务,耗时可能几个毫秒以上

Java Thread 使用场景

  • 涉及中等或大量的网络数据操作(包括上传和下载)
  • 需要在后台执行的 CPU 密集型任务
  • 当你想要在 UI 线程控制 CPU 占用率时

还有一个老生常谈的问题就是,千万不要在 UI 线程(主线程)执行网络操作。你需要使用上述两种方式之一来访问网络。

关键点

Java Thread 和 AsyncTask最关键的不同点在于,AsyncTask运行在 GUI 线程¹ 上,所以繁重的 CPU 任务都可能导致 UI 响应性下降。Java Thread 可以拥有不同的线程优先级,使用低优先级的线程来完成非实时运算任务能够很好地为 GUI 操作释放 CPU 时间。这是提高 GUI 响应性的关键点之一。

然而,正如很多 Android 开发者所了解的,你无法在后台线程更新 UI 组件,不然就会抛出异常。这对于 AsyncTask来说并不是什么大事² ,但是当你使用的是 Java Thread,那么你必须在你操作结束的时候使用post()来更新 UI³ 。


译者按原文查找资料注:

  1. AsyncTask必须在主线程加载,其中除了doInBackground(Object [])方法外,其余三个方法都在 UI 线程运行
  2. 基于第一点,AsyncTask可以在其余三个方法中更新 UI 组件
  3. 可以使用view.post()方法来更新 UI 组件,这个方法和使用Activity.runOnUiThread()方法区别不大

参看

前言

在上一篇文章手动实现轮播图(一):ViewPager 入门实践中,我们认识了ViewPager这个布局,也简单上手了一下。

接下来这篇文章,我们会进一步朝着轮播图的方向前进。

原来的文章末尾,我使用了 Glide 加载 Gif 图片作为轮播图的内容,所以现在也是基于那个代码继续下去的。

如果对这部分比较陌生,建议回去看一下文章末尾仓库地址里的代码哦

本文章中我们将会实现:

  • 循环滚动
  • 切换指示器
  • 定时切换

接下来就让我们开工吧。

1. 循环滚动

ViewPager虽然好用,但是并不原生支持循环滚动,也就是你:

  • 第一个往左滑,会跳到最后一个
  • 从最后一个往右滑,会调回第一个

我们之前实现的效果里,第一个就无法再往左滑了,最后一个就无法再往右滑了。这样轮播图就不是“轮”播了。

所以我们需要自己来实现循环滚动这个效果。

该怎么实现呢?目前也有比较成熟的三个做法:

多页面假循环

  • 创建很多个页面,即便我们真正需要展示的时候只有 5 个页面

    • 把起始点放在队列中间,如果到了要展示的第一个页面,继续往左的时候,我们把接下来就把页面设置为最后一个的样式
    • 这样不管用户往左还是往右滑,只要是正常情况下,用户都是滑不到头的,造成视觉上的循环

      • 正常 App 中,即便你使用一个这样的页面队列来显示,用户也没有耐心一直滑下去

假设我们现在创建了 1000 个页面的ViewPager,然后我们实际需要展示的只有 5 个页面,那么实现的效果如下:

我们把第一个展示的页面设置为 500,那用户需要滑动 499 才会到头。

pic

这样性能会不会很差?

  • 不会

因为虽然说的是“创建1000”个页面,但是实际上我们只是告诉ViewPagerAdapter我们会使用这么多个,不代表他会创建这么多个。

我们会在AdaptergetCount方法里返回 1000,这个方法只是帮助Adapter获取正确的position的,并不是真正创建出来。(通过阅读PagerAdapter 的源码得出)

记得前面我们说过的,FragmentPagerAdapter会默认帮我们创建三个页面,所以这里也只会创建三个页面,超过前中后的其他页面都会被回收。

其余两种实现方法

我们主要使用第一种,理解起来简单易懂,也没有明显的短板。

其余两种方法描述看下面这篇文章:Android实现真正的ViewPager【平滑过渡】...


介绍完实现思路,我们就可以开始实现了。

打开MainActivity.java,修改代码如下:

public class MainActivity extends AppCompatActivity {
    private static final int MAX_NUMBER = 1000;
    private static final int START_POSITION = MAX_NUMBER/2;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mViewPager.setAdapter(new FragmentPagerAdapter(fm) {

            // 存储通过 position 计算出正确的数组索引
            private int mIndex;

            @Override
            public Fragment getItem(int position) {
                mIndex = Math.abs(position - START_POSITION) % mStringList.length;

                if (position < START_POSITION && mIndex != 0){
                    mIndex = mStringList.length - mIndex;
                }

                return PageFragment.newInstance(mIndex);
            }

            @Override
            public int getCount() {
                return MAX_NUMBER;
            }


        });

        mViewPager.setCurrentItem(START_POSITION);

        ...
    }
}
  • 定义两个常量,分别是

    • MAX_NUMBER:页面总数,一共 1000 个
    • START_POSITION:起始的页面,从中间第 500 个开始
mIndex = Math.abs(position - START_POSITION) % mStringList.length;

if (position < START_POSITION && mIndex != 0){
    mIndex = mStringList.length - mIndex;
}
  • 计算当前位置position和起始位置(START_POSITION)的距离,然后把结果和真正要展示的页面数量(此处暂时使用mStringList的长度代替)取余

    • 距离有正负,所以取了绝对值。但是如果只是绝对值然后去取余的话,左滑的时候,就不是 1->5 -> 4 这样子,而是 1 ->2 ->3 这样。这是取余运算的结果,不熟悉的同学可以回忆一下取余的结果
    • 所以我们加了判断

      • 页面position大于起始位置 ,那就直接用相对距离取余
      • 如若小于起始位置,那么用实际页面数量减去取余结果,就可以实现倒数的效果了
@Override
public int getCount() {
    return MAX_NUMBER;
}
  • 此处告诉Adapter一共有多少个页面

记得设置起始页面哦:

mViewPager.setCurrentItem(START_POSITION);

这样我们的循环滚动就完成了~快试试看吧。

pic

2. 页面指示器

许多轮播图都有一个小指示器,用来标志当前的页面。我们现在就来做一个。

做了前面的循环滚动,这样的页面指示器原理应该不难理解。

思路是:

  • 创建控件样式

    • 选中的样式
    • 未选中的样式
  • 添加控件到视图里面
  • 当页面滑动的时候,修改指示器的样式

创建控件样式

res/drawable文件夹里,创建两个文件:

正常样式:dot_normal.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="5dp"
        android:height="5dp"/>
    <solid android:color="@android:color/holo_red_dark"/>
</shape>

被选中样式:dot_selected.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="5dp"
        android:height="5dp"/>
    <solid android:color="@color/colorPrimary"/>
</shape>

接着在activity_main.xml里加入一个LinearLayout布局,后面我们使用代码的方式把小点加入进去:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager_inside"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:background="@android:color/darker_gray"
        android:layout_centerInParent="true">
    </android.support.v4.view.ViewPager>

    <LinearLayout
        android:id="@+id/ll_inside"
        android:layout_below="@+id/view_pager_inside"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:orientation="horizontal"
        android:gravity="center"/>

</RelativeLayout>

添加控件到视图中

此处的代码思路来自Android ViewPager 无限循环左右滑动(可自动) 实现

回到MainActivity.java中,

public class MainActivity extends AppCompatActivity {
    private List<TextView> mTextViews;
    private LinearLayout mLinearLayout;

    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPager = findViewById(R.id.view_pager_inside);
        mLinearLayout = findViewById(R.id.ll_inside);

        initCircle();
        ....
    }

    ...
    private void initCircle() {
        mTextViews = new ArrayList<>();
        int d = 20;
        int m = 7;

        for (int i = 0; i < mStringList.length; i++){
            TextView textView = new TextView(this);
            if (i == 0){
                textView.setBackgroundResource(R.drawable.dot_selected);
            }else {
                textView.setBackgroundResource(R.drawable.dot_normal);
            }

            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(d, d);

            params.setMargins(m, m, m, m);
            textView.setLayoutParams(params);
            mTextViews.add(textView);
            mLinearLayout.addView(textView);
        }
    }
    ...
}
  • 定义两个变量

    • mTextViews:存放小点的列表

      • 我们的小点其实是由TetxView构成,然后背景颜色设置为圆形的
    • mLinearLayout:引用刚刚创建的LinearLayout布局
  • 创建一个initCIrcle()方法

    • 使用代码的方式创建TextView视图,为每个视图设置宽高、外边距和背景等属性

      • 背景样式就是刚刚创建的两个.xml文件
    • 使用 addView方法把小点添加到布局当中

Oncreate()方法中调用之后,我们就会看到小点已经出现了。

现在我们需要根据页面来修改样式,以达到指示器的作用。

public class MainActivity extends AppCompatActivity {

    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                changePoints(position % mStringList.length);
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });  
    }

    private void changePoints(int pos){
        if (mTextViews != null){
            for (int i = 0; i < mTextViews.size(); i++){
                if (pos == i){
                    mTextViews.get(i).setBackgroundResource(R.drawable.dot_selected);
                }else {
                    mTextViews.get(i).setBackgroundResource(R.drawable.dot_normal);
                }
            }
        }
    }
}
  • mViewPager添加一个状态监听器ViewPager.OnPageChangeListener

    • 重写onPageSelected()方法:该方法会在页面被选中的时候调用
    • 在该方法内,我们调用changePoint()方法来改变指示器的样式

我们在调用changePoint()的时候,传入的是position % mStringList.length。这里是有问题的。

如果直接使用positionmString.length进行取模,在这个例子里是没问题,因为我们起始位置(500)恰好是mString.length的倍数。所以此时会从 0 开始。但如果我们以后修改了起始位置亦或者修改了展示图片的数量的话,这里就会出错了。

所以我们还是使用和之前一样的方式来获得索引值。修改一下onPageSelected()方法:

private int mIndex;

@Override
public void onPageSelected(int position) {
    mIndex = Math.abs(position - START_POSITION) % mStringList.length;
    if (position < START_POSITION && mIndex != 0){
        mIndex = mStringList.length - mIndex;
     }
    changePoints(mIndex);

}

这里为了方便,就直接使用这段代码了。有时间的同学可以自己优化一下,提高复用率。

按照道理,现在应该就可以了。

indicator

3. 定时播放

轮播图的其中一个特点,就是定时播放。

我们已经实现了这么多效果了,定时播放应该也是小菜一碟。

我们可以使用Handle调用setCurrentItem()即可。

以下代码思路来自Android ViewPager 无限循环左右滑动(可自动) 实现

修改我们的MainActivity.java

private Handler mHandler = new Handler();

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        mHandler = new Handler();
        mHandler.postDelayed(new TimerRunnable(),5000);
    }

    class TimerRunnable implements Runnable{

        @Override
        public void run() {
            int curItem = mViewPager.getCurrentItem();
            mViewPager.setCurrentItem(curItem+1);
            if (mHandler!=null){
                mHandler.postDelayed(this,5000);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler = null; //此处在Activity退出时及时 回收
}

4. 修改过渡动画

调用ViewPager.setPageTransformer()方法即可自行设置动画。

让我们先新建一个动画类PhotoTransformer.java

package me.rosuh.android.viewpagernew;

import android.support.annotation.NonNull;
import android.support.v4.view.ViewPager;
import android.view.View;


public class PhotoTransformer implements ViewPager.PageTransformer {
    @Override
    public void transformPage(@NonNull View page, float position) {
        int pageWidth = page.getWidth();

        if (position < -1){
            page.setAlpha(1);
        }else if (position <= 1){
            page.setPivotX(position < 0f ? page.getWidth() : 0f);
            page.setPivotY(page.getHeight() * 0.5f);
            page.setRotationY(90f * position);

        }else {
            page.setAlpha(1);
        }
    }
}

然后为mViewPager设置动画:

...
FragmentManager fm = getSupportFragmentManager();

mViewPager.setPageTransformer(true, new PhotoTransformer());

mViewPager.setAdapter(...)

设置这个动画,最好把CardView的阴影属性设置为 0。
然后稍微修改一下布局。(在此不列出,可以到代码仓库自己看一下)。
下面是效果:

transformer

结语

本项目地址ViewPagerDemo

目前为止,我们的轮播图就已经做好了。

这两篇文章的目标读者是刚入门的同学,所以有许多地方还有改进的空间。

但是不碍于我们掌握。

文章作者毕竟经验不多,水平有限,所以缺漏在所难免,希望路过读到本文的前辈们不吝赐教,谢谢~

感谢一下参考文章和资料:

cat

简介

Viewpager是 Android 提供的布局管理器,常被用来实现左右滑动的页面、视图。

在实际工程中,有许多都是用来实现轮播图功能的。

今天,我们从零开始造一个简易轮播图组件。

本系列文章面向的读者,是刚学完 Android 教材的初学者,旨在:

  • 简单介绍ViewPager原理并如何快速上手
  • 使用简单的代码结构,完成一个初级的轮播图组件

文章作者毕竟经验不多,水平有限,所以缺漏在所难免,希望路过读到本文的前辈们不吝赐教,谢谢~

接下来,我们就从Viewpager是什么开始,慢慢来了解他。

1. Viewpager 上手

官方开发文档:android.support.v4.view.ViewPager

  • ViewPager是一个布局管理器,可以作为根布局

    • 因为他继承于ViewGroup,常见的布局管理器还有FrameLayout, LinearLayout
    • 当他作为根布局时,每一个页面都将占据整个布局
  • ViewPager该怎么使用

    • 在布局文件中添加一个<ViewPager>标签,此位置作为ViewPager容器主体所在
    • 创建一个新的布局文件,作为内嵌页面的布局

      • 如果使用fragment的话,我们只需要创建一个模板,之后所有内嵌页面都使用这个模板来生成即可
      • 如果单单使用布局文件,那么我们每一个页面项都要创建一个布局文件,之后手动添加ViewPager容器
      • 所以本文章均使用fragment来实现
    • activity中,实例化ViewPager
    • ViewPager设置Adapter

      • 类似于RecyclerView,我们也是使用Adapter来和ViewPager进行通信
      • 这样大大方便了我们使用

上述步骤中,前几步几乎是组件/布局实例化的常规操作,所以我们真正要做的其实非常少。

接下来我们开始动手来使用ViewPager

创建 ViewPager 容器和子页面布局文件

我们新建一个项目之后,打开默认创建的activity_main.xml布局文件中,将内容改为以下代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager_inside"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:background="@android:color/darker_gray"
        android:layout_centerInParent="true">
    </android.support.v4.view.ViewPager>
</RelativeLayout>

可以看到,布局文件中仅有一个根布局RelativeLayout和一个ViewPager

这里的ViewPager就是容器主体所在。

接着我们创建嵌入的页面布局文件:

新建一个view_pager_fragment.xml文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:gravity="center"
    android:orientation="vertical">

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="300dp"
        android:layout_height="300dp"
        app:cardCornerRadius="10dp"
        android:elevation="5dp">
        <TextView
            android:id="@+id/text_view_fragment"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </android.support.v7.widget.CardView>

</LinearLayout>

这里面是常规布局,有一个卡片CardView和内藏一个的TextView

到时候,滑动的每一个页面的布局模板都来自这个文件,我们只需要在代码里稍微修改,就可以生成特定的页面了。

现在,我们回到MainActivity.java文件中,实例化我们刚刚的ViewPager

public class MainActivity extends AppCompatActivity {
    // 定义一个 Viewpager 变量
    private ViewPager mViewPager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // 实例化 ViewPager
        mViewPager = findViewById(R.id.view_pager_inside);
    }
}

接下来我们该干什么呢?当然是为ViewPager添加页面了。

那么页面从哪里来呢?当然是我们之前创建的那个布局view_page_fragment.xml了。

  • 我们的ViewPager主体位于activity_main.xml布局中

    • 我们在MainActivity.java中使用setContentView(R.layout.activity_main); 设置两者关联
    • 然后我们可以在MainActivity.java里面实例化ViewPager并使用它
  • 同理,我们要创建一个Fragment,将它和view_page_fragment.xml关联起来,并在它里面实例化页面的布局

不理解Fragment的同学,可以看一下文档里的 片段 哦。

创建一个PageFragment.java类,继承于android.support.v4.app.Fragment,这里特别注意要使用v4包里的Fragment

现在这个类里空荡荡,让我们来填充一些有意思的内容。

  1. 关联PageFragment.javaview_page_fragment.xml

使用Alt + Insert,选择Override Methods,然后重写onCreateView如下:

    private TextView mTextView;
    private CardView mCardView;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.view_pager_fragment, container, false);

        mTextView = view.findViewById(R.id.text_view_fragment);
        mCardView = view.findViewById(R.id.card_view);
        return view;
    }

我们使用了LayoutInflater来将view_page_fragment.xml加载为代码里的View对象,然后再从view对象里,找到我们放置的两个组件:CardViewTextView

如果不理解LayoutInflater可以看看如下两位的文章:

这样就算是关联起来了,系统在创建PageFrament.java对象的时候,就会实例化view_page_fragment.xml布局了。

接着我们为PageFragment.java创建一个静态生成器(方法)。为什么要静态生产类呢?

因为我们每生成一个页面,其实就是创建一个PageFragment.java的对象,然后我们还要向这个对象传递数据。

为了不做重复的工作,我们写一个静态生成器,这样每次外部类只要调用这个静态生成器,就可以很简单地创建对象了。

看看代码:

public class PageFragment extends Fragment {

    public static Fragment newInstance(){

        return new PageFragment();
    }
    public View onCreateView ...
}

如果你写过静态Intent生成方法,相信这个类生成器也很容易理解了。

上面代码就是在返回时,先创建一个PageFragment对象再返回去。就这一句代码,有必要写一个静态方法吗?

当然有,因为我们还没有把他真正的用处挖掘出来呢!

前面说到的,我们之所以只需要创建一个布局模板文件,而不需要每一个页面就定制一个,就是我们要在代码里动态定制页面。

我们这里子页面模板里,只有一个TextView可以写东西,所以我们用它来作为区分页面的标志,比如T1T2这样。

那问题就是,我们如何动态定制页面呢?

我们来看看现在的情况吧:

pic

可以看到,这是典型的 MVC 结构,在这里面呢,PageFragment唯一地通过MainActivity.java来创建,虽然我们还没有实现这一步。

也即是说,我们要在这一步里,向PageFragment传递定制化的数据,比如页面一传递T1,页面二传递T2这样子。

接着在PageFragment只需要使用同一套代码就可以生成不同的页面了。

问题的难点在于如何向一个Fragment传递数据。当然,这样的文章已经写了很多了,相信你稍微搜索一下,就知道我们即将使用的是Fragment Arguement的方法。其实就是在fragment对象上附加一个参数。

这种方法是不是很像Intent的附加参数呢?

下面是实现代码:

public class PageFragment extends Fragment {

    private static final String ARGS_TITLE = "argsTitle";
    private CardView mCardView;
    private TextView mTextView;

    public static Fragment newInstance(String title){
        Bundle args = new Bundle();
        args.putString(ARGS_TITLE, title);
        PageFragment pageFragment = new PageFragment();
        pageFragment.setArguments(args);
        return pageFragment;
    }
    public View onCreateView ...
}

在这里面,我们使用了Bundle对象来存储要传递的数据,然后使用setArguement()方法来把参数附加到新建的pageFragment对象里面。

最后返回这个对象即可。

接下来我们就可以在MainActivity.java里面使用这个静态生成器。(我只列出了新增的代码哦)

public class MainActivity extends AppCompatActivity {
    ...

    private String[] mStringList = {
            "T1", "T2", "T3", "T4", "T5"
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        FragmentManager fm = getSupportFragmentManager();      
        mViewPager.setAdapter(new FragmentPagerAdapter(fm) {

            @Override
            public Fragment getItem(int position) {

                String title = mStringList[position];

                return PageFragment.newInstance(title);
            }

            @Override
            public int getCount() {
                return mStringList.length;
            }


        });
    }
}

这里我们先定义了一个字符串数组,来存储文字字符串。也就是之前图片的【模型】区域。

FragmentManager fm = getSupportFragmentManager();

这句代码是获取一个FragmentManager,也就是fragment的管理器。接下来在ViewPager中需要用这个管理器来管理fragment。(别担心,这里系统已经帮你做好了,你只要传入一个管理器就行。

mViewPager.setAdapter(new FragmentPagerAdapter(fm) {
    ...
});

这一句也好理解,前面说了,ViewPager也需要一个对应的Adapter来和他通信,幸运的是系统已经为我们提供了两个非常好用的FragmentAdapter

  • FragmentPagerAdapter

    • 会提前自动创建:前中后,三个页面
    • 适合页面布局简单的情况
  • FragmentStatePagerAdapter

    • 只会创建一个页面
    • 适合页面布局复杂的情况

所以我们这里使用了FragmentPagerAdapter咯。

使用这个FragmentPagerAdapter,最少只需要重写两个方法:

  • getItem()

    • 通过position参数,返回一个创建好的页面
    • 我们就要在这里面做页面的创建工作哦
  • getCount()

    • 要创建的页面的数量

理解到这里,我们只需要在这段代码后面,轻轻加上一句:

mViewPager.setCurrentItem(0);

然后构建、运行,这个 Demo 就做好啦!

快试试效果吧~

试完了吗?是不是感觉哪里不对劲?

TextView 呢?

对啊,因为你虽然在newInstance里存放了数据,但是你并没有取出来呀~

来到PageFragment里取出来吧。

@Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.view_pager_fragment, container, false);
        mTextView = view.findViewById(R.id.text_view_fragment);
        mCardView = view.findViewById(R.id.card_view);

        String title = getArguments().getString(ARGS_TITLE);
        mTextView.setText(title);

        return view;
    }

现在不就可以了嘛~

什么?你嫌一个TextView太单调?...

那你干嘛不加一个ImageView进去啊,然后传入一些令人心旷神怡的图片还不是美滋滋?

Viewpager


接下来的文章会实现无限循环滑动页面指示器,敬请期待~

前言

因为早上想用咕咕机(Memobird)打印一下天气,发现他还有 Web 版本。然后想着能不能打包成一个.exe,直接丢到桌面执行,不想每次都打开浏览器、网页什么的...

然后了解了一下ElectronNW.js。然后顺着用了后者尝试打包了一下,发现还是挺简单的。

因为个人不需要添加过多的功能,所以使用门槛较低的 NW.js 来实现。如果你需要添加更多自定义的功能,可以考虑使用前者来完成你的开发工作。

下面是过程。

1. 软件准备

不太喜欢每到一步就去下载一个软件,个人喜欢把软件和环境先准备好。

本文章环境为 Win10,使用 Linux 的同学自己应该可以解决这些小问题。

  • NW.js(*必要)

    • 本次工作的主体软件
    • 需要下载,然后解压
    • 解压后的目录为我们本次的工作根目录
  • Enigma Virtual Box(*必要)

    • 打包工具,后面用它来打包成可执行.exe文件
    • 需要下载并安装
  • IconWorkshop-Pro(*可选)

    • 用于创建程序的图标资源,需要下载并安装
    • 可以创建各种尺寸一整套图标,如果你没有这样的需求,自己用随便找一张图片转为icon格式的也可以
    • 或者直接使用默认的图标
  • Resource Hacker(*可选)

    • 用于修改程序的 icon
    • 需要下载并安装

2. 打包

第一步里,我们解压了 NW.js 的压缩包,现在我们进入该目录。然后着手开始我们的工作。

pic1

2.1 创建一个项目文件夹

在当前目录下创建一个文件夹并进入。文件夹命名随意,比如我命名为memobird

pic2

接着我们在当前目录下,创建两个文件:index.htmlpackage.json

pic3

  • index.html

    • 打包后程序的入口文件,你可以在这个文件里设置一个跳转链接到目标网站

我的内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>memobird</title>
</head>
<body>
<script>
    window.location.href = "http://w.memobird.cn/cn/w/login.aspx";
</script>
</body>
</html>

可以看到,我设置了一个跳转,跳到咕咕机的 Web 版页面。你只需要把你的目标网站链接替换进去就行了。

  • package.json

    • NW.js 的配置文件

我的内容如下:

{
    "name": "Memobird",
    "version": "0.01",
    "main": "index.html",
    "window": {
      "width": 1024,
      "height": 768,
      "title": "Memobird"
    }
  }

此处只有几个参数,非常简单。

  • name

    • 应用名称
  • version

    • 版本号
  • main

    • 程序入口页面
  • windows

    • 程序窗口设置

这两个文件编辑好之后,将这两个文件打包为一个.zip格式的压缩包,并压缩包后缀改为.nw

pic

图示为使用 7-zip 压缩软件,你可以使用任何能达到同样效果的压缩软件

pic

pic

接着,把.nw后缀的文件,复制到上一目录,也即是我们工作的根目录。

pic

2.2 修改图标

如果你不需要修改图标,那么可以直接跳过这一步。我们在这里需要先修改nw.exe的图标,因为待会用nw.exe打包之后,默认图标就是nw.exe的图标。

打开IconWorkshop软件,不需要选择创建icon,而是打开一张你选好的图片会比较快。当然,你也可以自己从零开始创建一个icon

pic

在图片窗口左上角,找到创建icon的功能按钮。

pic

在弹出的对话框中,你可以自定义图标的信息。如果没什么要求直接确定即可。

点击确定之后,它会回到刚刚的图标窗口,这时候需要保存一下。

pic

这样我们的图标就制作完成了。

接着使用 Resource Hacker 修改 nw.exe 的图标。

  • 打开 Resource Hacker
  • 直接将 nw.exe拖入程序窗口
  • 在左侧目录中找到Icon Group目录,并在子文件中找到默认图标

pic

  • 接着右击该图标,选择 replace icon

    • 然后选择刚刚我们做的图标并替换即可

pic

最后保存更改并退出。

虽然现在已经更改了,但是因为 windows 存在图标缓存,所以并没有发生变化。你可以将nw.exe复制到其他文件夹,就会看到变化了

2.3 打包文件

在我们工作目录下打开cmd,输入如下格式的命令:

copy /b nw.exe+memobird.nw mimobird.exe

pic

  • memobird.nw是我们创建并复制过来的
  • memobird.exe 将要是打包后并生成的文件的名字

你根据你自己的文件吗进行修改即可。

现在便可以打开memobird.exe看看效果了。

目前该文件只能在当前目录下运行,因为他需要一些资源文件。

接着,我们便把资源文件也一并打包进去。

3. 构建单文件执行程序

  • 打开 Enigma Virtual Box

    • “封包主程”选择刚刚生成的 memobird.exe

接着打开我们的工作目录,把所有文件都拖进下面的空白中:

pic4

其中:memobird.exe, memobird.nw都是不需要的,你可以自己删掉。

然后点击“执行封包”按钮开始封包。

pic5

等待封包完成即可。

这样,我们就简单并且快速地完成了创建了一个桌面程序,内部挂载的是原站点。

实际上类似于浏览器的模式。

pic6


参看