起因

看到众多大神纷纷有了自己的开源项目,于是自己琢磨着也想做一个开源项目来学习下,因为每次无聊必刷的app就是今日头条,评论简直比内容都精彩,所以我打算仿今日头条来练练手,期间也曾放弃过,也遇到很多坑,拿出来跟大家分享一下,喜欢的记得给个Star,当作是给我的鼓励和动力吧。

源码链接

https://github.com/yewei02538/TodayNews

无图言屌

第三方库

  • BaseRecyclerViewAdapterHelper
  • ImageLoader
  • Retrofit
  • RxJava
  • ButterKnife
  • MultipleTheme
  • ColorTrackView
  • Gson
  • JieCaoVideoPlayer

技术要点

  • 主要是一些第三方库的使用
  • 首页顶部导航使用的hongyang大神的ColorTrackView然后做了一下封装来实现滑动渐变效果
  • 多种Item布局展示->BaseRecyclerViewAdapterHelper
  • 日夜间模式切换->MultipleTheme
  • 个人中心 自定义ScrollView实现下拉图片放大
  • 新闻详情我采用的是RecyclerView添加头的方式添加WebView(当然是Adapter里面添加),加载页面成功之后获取评论信息,点击评论图标滑动至评论第一条,这里我是调用recyclerView.smoothScrollToPosition(1);
  • 视频播放我使用的是JieCaoVideoPlayer,一群大牛封装的代码,底层实际使用ijkplayer,视频源均使用非正常手段获取,视频源地址分析请看我的另一篇博客手撸一个今日头条视频下载器

问题1

在使用MultipleTheme的时候唯一的缺陷就是需要在布局里面大量使用到自定义控件,这对于我们的项目而言,布局看着很冗余,也有点恶心。。我有时候就在想,那我可不可以写原生控件,然后在特定的时机来个偷梁换柱换成我们的自定义控件呢?(比如我们布局写RelativeLayout—转换成MyRelativeLayout),似乎好像是可以的。

思路1

当时想到一个最简单最快实现的方法,也就是替换,我在布局里面写原生控件,然后在用工具全局替换成我们的自定义控件,但是假如我们换了包名,那就需要重新替换,这无疑是不易扩展的,所以这个方法放弃掉

思路2

不知道大家有木有发现就是,我们在布局里面写上ButtonImageViewTextView等这些控件的时候,在5.0以上运行的时候实际变成了AppCompatButtonAppCompatImageViewAppCompatTextView(debug或者打印对象就可以看到实际的类型),在当我们运行的时候就这样悄无声息的给替换了,那系统又是怎么做到的?那只要找到它的实现方法,我们的问题不就迎刃而解了吗?

于是我找到系统替换的代码(以下代码全部基于Api23)

AppCompatViewInflater.Java

public final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy// by using the parent's contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = themifyContext(context, attrs, readAndroidTheme, readAppTheme);}if (wrapContext) {context = TintContextWrapper.wrap(context);}View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versionsswitch (name) {case "TextView":view = new AppCompatTextView(context, attrs);break;case "ImageView":view = new AppCompatImageView(context, attrs);break;case "Button":view = new AppCompatButton(context, attrs);break;case "EditText":view = new AppCompatEditText(context, attrs);break;case "Spinner":view = new AppCompatSpinner(context, attrs);break;case "ImageButton":view = new AppCompatImageButton(context, attrs);break;case "CheckBox":view = new AppCompatCheckBox(context, attrs);break;case "RadioButton":view = new AppCompatRadioButton(context, attrs);break;case "CheckedTextView":view = new AppCompatCheckedTextView(context, attrs);break;case "AutoCompleteTextView":view = new AppCompatAutoCompleteTextView(context, attrs);break;case "MultiAutoCompleteTextView":view = new AppCompatMultiAutoCompleteTextView(context, attrs);break;case "RatingBar":view = new AppCompatRatingBar(context, attrs);break;case "SeekBar":view = new AppCompatSeekBar(context, attrs);break;}if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check it's android:onClickcheckOnClickListener(view, attrs);}return view;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

当我们在xml写的那些布局映射成对象的时候,都会调用到这里来转换成对应的AppCompat。

偷梁换柱的关键点我们找到了,那如何找到这个入口呢?

其实当我们加载布局的时候最终都会用LayoutInflater来加载,所以我打算从这里入手,看源码我发现有一个接口可以利用->Factory,这个接口有一个方法

    public interface Factory {/*** Hook you can supply that is called when inflating from a LayoutInflater.* You can use this to customize the tag names available in your XML* layout files.* * <p>* Note that it is good practice to prefix these custom names with your* package (i.e., com.coolcompany.apps) to avoid conflicts with system* names.* * @param name Tag name to be inflated.* @param context The context the view is being created in.* @param attrs Inflation attributes as specified in XML file.* * @return View Newly created view. Return null for the default*         behavior.*/public View onCreateView(String name, Context context, AttributeSet attrs);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

果然功夫不负有心人,如果我们实现了这个接口,最终加载布局的时候那么就会调用onCreateView在这里面来实现偷梁换柱替换成我们的自定义控件

ok,入口和关键代码都找到了,剩下的就是撸代码了

public class SkinFactory implements LayoutInflaterFactory {private AppCompatActivity mActivity;public SkinFactory(AppCompatActivity activity) {mActivity = activity;}@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {View view = null;//是否需要替换成自定义Viewboolean isColorUi = attrs.getAttributeBooleanValue("http://schemas.android.com/apk/res-auto", "isColorUi", false);if (!isColorUi) return delegateCreateView(parent, name, context, attrs);switch (name) {case "TextView":view = new ColorTextView(context, attrs);break;case "ImageView":view = new ColorImageView(context, attrs);Logger.i("ImageView 转换成"+view.getClass().getSimpleName());break;case "RelativeLayout":view = new ColorRelativeLayout(context, attrs);break;case "LinearLayout":view = new ColorLinearLayout(context, attrs);break;case "View":view = new ColorView(context, attrs);break;}if (view == null) {view = delegateCreateView(parent, name, context, attrs);}return view;}private View delegateCreateView(View parent, String name, Context context, AttributeSet attrs) {AppCompatDelegate delegate = mActivity.getDelegate();return delegate.createView(parent, name, context, attrs);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

这里isColorUi做了一个标示,因为有的是不需要转换的,如果不转换,直接走系统的创建View流程

关键代码写好了,下面是入口

BaseActivity.java

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);LayoutInflaterCompat.setFactory(layoutInflater, new SkinFactory(this));}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

原本以为这样就完美的解决了,没想到又引出了下一个问题

Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflaterat android.view.LayoutInflater.setFactory2(LayoutInflater.java:317)at android.support.v4.view.LayoutInflaterCompatLollipop.setFactory(LayoutInflaterCompatLollipop.java:28)at android.support.v4.view.LayoutInflaterCompat$LayoutInflaterCompatImplV21.setFactory(LayoutInflaterCompat.java:55)at android.support.v4.view.LayoutInflaterCompat.setFactory(LayoutInflaterCompat.java:85)at me.weyye.todaynews.base.BaseActivity.setLayoutInflaterFactory(BaseActivity.java:70)at me.weyye.todaynews.base.BaseActivity.onCreate(BaseActivity.java:60)at android.app.Activity.performCreate(Activity.java:6910)at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2746)at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864) at android.app.ActivityThread.-wrap12(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

于是找到这个方法

public void setFactory(Factory factory) {if (mFactorySet) {throw new IllegalStateException("A factory has already been set on this LayoutInflater");}if (factory == null) {throw new NullPointerException("Given factory can not be null");}mFactorySet = true;if (mFactory == null) {mFactory = factory;} else {mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

mFactorySet=true的时候就会抛出这个错误,可是我并没有去set,那么可能是系统set了,对,没错,不然它怎么转换成AppCompat呢。

那么我只需要用反射把mFactorySet改成false就可以了

于是乎我修改了下原来的代码

BaseActivity.java

    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setLayoutInflaterFactory();}public void setLayoutInflaterFactory() {LayoutInflater layoutInflater = getLayoutInflater();try {Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");mFactorySet.setAccessible(true);mFactorySet.set(layoutInflater, false);LayoutInflaterCompat.setFactory(layoutInflater, new SkinFactory(this));} catch (Exception e) {e.printStackTrace();}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

先利用反射改成false然后在设置上去,这样就不会报错了

问题2

好吧,几经周折终于完成了日夜间切换,但是当我滑动新闻列表的时候,又一个问题出来了…

当点击切换成夜间主题后,列表滑动后有的还是白天的主题,这很明显是RecyclerView复用的问题,我的思路是当点击切换主题后清除掉复用的View,这样就不会出现这种问题。怎么清除呢?好像RecyclerView没有直接给我们方法,所以我得去源码好好看看,发现RecyclerView里面有个内部类Recycler用来管理复用和回收的类,而且有clear方法,

public final class Recycler {.../*** Clear scrap views out of this recycler. Detached views contained within a* recycled view pool will remain.*/public void clear() {mAttachedScrap.clear();recycleAndClearCachedViews();}...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

看代码好像是我所需要的,于是找到这个类对应的变量mRecycler,可惜的是private并且没有get方法,那就只好反射咯~

        RecyclerView recyclerView = (RecyclerView) rootView;try {Field mRecyclerField = RecyclerView.class.getDeclaredField("mRecycler");mRecyclerField.setAccessible(true);Method clearMethod = RecyclerView.Recycler.class.getDeclaredMethod("clear");clearMethod.setAccessible(true);clearMethod.invoke(mRecyclerField.get(recyclerView));} catch (Exception e) {e.printStackTrace();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

ok,成功解决!

未完待续…

TODO

  • 加入未写界面以及功能
  • 逻辑代码的整理

Android仿今日头条的开源项目相关推荐

  1. Android 仿今日头条的开源项目

    前言 看到众多大神纷纷有了自己的开源项目,于是自己琢磨着也想做一个开源项目来学习下,因为每次无聊必刷的 app 就是今日头条,评论简直比内容都精彩,所以我打算仿今日头条来练练手,期间也曾放弃过,也遇到 ...

  2. Android 仿今日头条频道管理(下)(GridView之间Item的移动和拖拽)

    前言 上篇博客我们说到了今日头条频道管理的操作交互体验,我也介绍了2个GridView之间Item的相互移动.详情请參考:Android 仿今日头条频道管理(上)(GridView之间Item的移动和 ...

  3. 转载 Android仿今日头条详情页实现

    转载自@ice_Anson Android仿今日头条详情页实现 源码地址: Android仿今日头条详情页实现 github源码地址 动态图 最近项目有个需求,需要实现一个和今日头条新闻详情页一样的体 ...

  4. android仿今日头条个人中心页面

    android仿今日头条个人中心页面 效果图 实现步骤: 自定义ScrollView,添加一个反弹的动画 代码: package com.example.administrator.gerenzhon ...

  5. Android 仿今日头条首页标题栏效果

    今天带来的是仿今日头条首页的联动滑动效果,废话不多说,先上效果图: 思路: 做这个我们需要实现的效果有 1.滑动内容区域,标题栏会有变化来显示当前所处的位置. 2.点击标题栏,内容区域也会随着滑动并跳 ...

  6. android 今日头条加载动画,Android 仿今日头条简单的刷新效果实例代码

    点击按钮,先自动进行下拉刷新,也可以手动刷新,刷新完后,最后就多一行数据.有四个选项卡. 前两天导师要求做一个给本科学生预定机房座位的app,出发点来自这里.做着做着遇到很多问题,都解决了.这个效果感 ...

  7. Android 仿今日头条评论时键盘自动弹出的效果

    Android 仿今日头条评论时键盘自动弹出的效果:当点击评论时,弹出对话框,同时弹出软键盘,当点击返回键时,将对话框关闭,不只是关闭软键盘. 效果图: 对这个对话框设置一个style效果: < ...

  8. Android仿今日头条首页的顶部标签栏和底部导航栏

    Android仿今日头条首页的顶部标签栏和底部导航栏 先是底部导航栏TextView+ImageView+Fragment: 效果图: activity_main.xml布局: <?xml ve ...

  9. Android仿今日头条开源项目

    起因 看到众多大神纷纷有了自己的开源项目,于是自己琢磨着也想做一个开源项目来学习下,因为每次无聊必刷的app就是今日头条,评论简直比内容都精彩,所以我打算仿今日头条来练练手,期间也曾放弃过,也遇到很多 ...

最新文章

  1. BIG-IP系统进程介绍
  2. C语言中Static和Const关键字的的作用
  3. 06 | 哨兵机制: 主库挂了, 如何不间断服务
  4. boost::condition_variable相关的测试程序
  5. 解决cookie写入问题
  6. 将在本地创建的Git仓库push到Git@OSC
  7. php curl CURLOPT_TIMEOUT_MS 小于1秒 解决方案
  8. 寻找互联网创业的时间点规律
  9. WebSocket(1)---WebSocket介绍
  10. python银行系统-python实现简单银行管理系统
  11. 各种电信安卓手机玩机宝典!——转自天翼圈in189
  12. Jenkins中Maven构建Archiving会重命名jar
  13. English trip V2 - 8 Holidays and Birthdays Teacher:Julia Key: at on in
  14. 大疆云台和华为P30_全面分析曝光大疆云台3和mobile有没有区别?哪个好?优缺点内幕透露...
  15. ACC算法学习笔记(六):ASPICE开发流程
  16. 时下热门的 AR 广告怎么做?广告创意和投放全攻略来了
  17. 内存延时cl_简单解析,什么是“CL延迟”
  18. 中文字典排序与多音字处理
  19. 设计师找灵感,这5个网站就够了
  20. C++多线程同时读同一文件

热门文章

  1. 8QAM 调制解调 代码
  2. The producer service state not OK, CREATE_JUST
  3. 经典Robocode例子代码--SnippetBot
  4. [卓意听书]6月感恩活动,Q币送不停!
  5. 蚂蚁金服彭翼捷:金融科技不止用来改良 更要用来改变
  6. 2017江苏高职计算机分数线,2017—2019江苏高考招生投档分数线(体育高职专科院校).docx...
  7. html2canvas微信头像后,微信小程序使用canvas在真机上不显示用户头像问题(不显示网络图片)...
  8. 【MQTT】Windows:安装MQTT
  9. 写一些生活的琐事(纯属发泄)
  10. 你的域名前要加www吗?