开源项目学习(微阅)
  mrMLeSzj1mp0 2023年11月02日 40 0


概述

今天开始学习一个新的项目,微阅,先看看效果图

开源项目学习(微阅)_加载

开源项目学习(微阅)_加载_02

然后我们看看项目的组织结构

开源项目学习(微阅)_ide_03

这个项目也是采用MVP模式开发的,api包中是访问数据的接口,相当于M层,presenter包中相当于P层,调用api中的接口去访问数据,然后交给View层显示。

笔记

更改导航栏的颜色(5.0以上才支持),效果图如下

开源项目学习(微阅)_android_04

相关代码如下

//更改底部导航栏的颜色,只有版本大于21,也就是5.0才支持
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (SharePreferenceUtil.isChangeNavColor(this))
getWindow().setNavigationBarColor(vibrantColor);
else
getWindow().setNavigationBarColor(Color.BLACK);//黑色
}

FloatingActionButton

开源项目学习(微阅)_ide_05

向上滑动消失,向下滑动出现

先看布局文件

<android.support.design.widget.FloatingActionButton
android:id="@+id/fabButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
//按钮位置
android:layout_gravity="bottom|end"
//边距
android:layout_margin="10dp"
//按钮图片
android:src="@drawable/ic_arrow_upward_white_24px"
//按钮背景颜色
app:backgroundTint="@color/colorAccent"
//边框宽度,最好设置上
app:borderWidth="0dp"
//Z方向高度
app:elevation="6dp"
//按钮尺寸
app:fabSize="normal"
// 自定义动作 app:layout_behavior="name.caiyao.microreader.ui.view.ScrollAwareFABBehavior"
//按下以后Z方向的平移距离
app:pressedTranslationZ="12dp"
//按下波纹的颜色
app:rippleColor="#33728dff" />

接下来看看这个自定义的动作

public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
//为了能在xml中使用
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super();
}
// 指明我们希望处理垂直方向的滚动事件。
// 滚动事件同样是由本类处理,见下面的onNestedScroll()方法。
@Override
public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
final View directTargetChild, final View target, final int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
|| super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
// 检查Y轴的距离,决定是显示还是隐藏FAB。
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
//向上滑动时隐藏FAB
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
child.hide();
//向下滑动时显示FAB
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
child.show();
}
}
}

更改状态栏颜色
首先说明,只有4.4及其以上版本才支持更改状态栏颜色,比如半透明或者全透明,也就是我们所说的沉浸式状态栏。在4.4之前状态栏一直是黑色的,在4.4中带来了 windowTranslucentStatus 这一特性,因此可以实现给状态栏设置颜色,大概代码如下

public static void setColor(Activity activity, int color, int statusBarAlpha) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//需要设置这个 flag 才能调用 setStatusBarColor 来设置状态栏颜色
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
//5.0以上提供setStatusBarColor这个api设置状态栏颜色
activity.getWindow().setStatusBarColor(calculateStatusColor(color, statusBarAlpha));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 生成一个状态栏大小的矩形
View statusView = createStatusBarView(activity, color, statusBarAlpha);
// 添加 statusView 到布局
//获取decorView,其实是一个framelayout
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView().findViewById(android.R.id.content);
decorView.addView(statusView);
setRootView(activity);
}
}

夜间模式
在2016年的2月24日,google的Android开发团队发布了:

compile 'com.android.support:appcompat-v7:23.2.0'

加入的新的东西,AppCompat DayNight theme和 Bottom Sheets。

AppCompat DayNight theme

开源项目学习(微阅)_ide_06

先关代码如下

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {//必须要判断,因为当设置了模式以后,savedInstanceState就不为空了
if (Config.isNight) {
getDelegate().setLocalNightMode(
AppCompatDelegate.MODE_NIGHT_YES);
} else {
getDelegate().setLocalNightMode(
AppCompatDelegate.MODE_NIGHT_NO);
}
// 调用 recreate() 使设置生效
recreate();
}
//setContentView必须放在最后
setContentView(R.layout.activity_main);
}
public void click(View view){
Config.isNight = !Config.isNight;
finish();
startActivity(new Intent(this,MainActivity.class));
}

需要注意的是,当前的主题要继承Theme.AppCompat.DayNight

Bottom Sheets

​Android Bottom Sheet详解​

ActionBarDrawerToggle

开源项目学习(微阅)_ide_07

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawerLayout1 = (DrawerLayout) findViewById(R.id.draw_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawerLayout1, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawerLayout1.addDrawerListener(toggle);
toggle.syncState();

设置fragment切换时的方向(只有5.0以上才有效果)

开源项目学习(微阅)_android_08

Slide slideTransition;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0以上才有效果
//Gravity.START部分机型崩溃java.lang.IllegalArgumentException: Invalid slide direction
slideTransition = new Slide(Gravity.BOTTOM);
slideTransition.setDuration(4000);
fragment.setEnterTransition(slideTransition);
fragment.setExitTransition(slideTransition);
}

Butterknife在fragment中使用是要在onDestroyView中去除绑定

mUnbinder = ButterKnife.bind(this, view);
.......

@Override
public void onDestroyView() {
super.onDestroyView();
//Fragment中要去除绑定
mUnbinder.unbind();
mWeixinPresenter.unsubcrible();
}

PopupMenu

开源项目学习(微阅)_加载_09

PopupMenu popupMenu = new PopupMenu(mContext, holder.btnWeixin);
popupMenu.getMenuInflater().inflate(R.menu.pop_menu, popupMenu.getMenu());

popupMenu.show();

给RecyclerView条目添加进入动画

开源项目学习(微阅)_加载_10


在adapter的onBindViewHolder方法里面调用如下方法

//传入当前条目和位置
runEnterAnimation(holder.itemView, position);

private void runEnterAnimation(View view, int position) {
view.setTranslationY(ScreenUtil.getScreenHight(mContext));
view.animate()
.translationY(0)
.setStartDelay(100 * (position % 5))
.setInterpolator(new DecelerateInterpolator(3.f))
.setDuration(4000)
.start();
}

如何判断RecyclerView滑动到底部了,该加载第二页数据了

swipeTarget.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) //向上滑动到底部
{
visibleItemCount = mLinearLayoutManager.getChildCount();
totalItemCount = mLinearLayoutManager.getItemCount();
pastVisiblesItems = mLinearLayoutManager.findFirstVisibleItemPosition();
//当前可见条目数加上之前的条目总数=总得条目数,该加载第二页了
if (!loading && (visibleItemCount + pastVisiblesItems) >= totalItemCount) {
loading = true;
onLoadMore();
}
}
}
});

设置页面进入和退出缩放动画

开源项目学习(微阅)_android_11


在进入的activity的oncreate方法中​​overridePendingTransition(R.anim.zoomin, R.anim.zoomout);​

下面是动画的布局文件

//zoomin
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator" >
<scale
android:duration="@android:integer/config_mediumAnimTime"
android:fromXScale="2.0"
android:fromYScale="2.0"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
//zoomout
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:zAdjustment="top">
<scale
android:duration="@android:integer/config_mediumAnimTime"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale=".5"
android:toYScale=".5" />
<alpha
android:duration="@android:integer/config_mediumAnimTime"
android:fromAlpha="1.0"
android:toAlpha="0" />
</set>

webview缓存功能
WebView中存在着两种缓存:网页数据缓存(存储打开过的页面及资源)、H5缓存(即AppCache)。

if (!NetWorkUtil.isNetWorkAvailable(this))
//加载缓存,不管是否过期
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
else
//加载缓存,过期就不加载
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setUseWideViewPort(true);
webSettings.setDomStorageEnabled(true);
webSettings.setDatabaseEnabled(true);
webSettings.setBuiltInZoomControls(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
webSettings.setAppCachePath(getCacheDir().getAbsolutePath() + "/webViewCache");
webSettings.setAllowFileAccess(true);
//开启AppCache
webSettings.setAppCacheEnabled(true);
webSettings.setLoadWithOverviewMode(true);

注意:上面有句代码我们手动设置了webview的缓存目录,但是实际当我们去文件目录下发现这个目录并不存在,可能是因为webview自己会创建缓存目录吧,这个目录名称为app_webview

开源项目学习(微阅)_android_12

当条目中出现复用导致错误显示的时候,可以使用SparseBooleanArray解决

if (mSparseBooleanArray.get(Integer.parseInt(itHomeItem.getNewsid()))){
holder.btnDetail.setBackgroundResource(R.drawable.ic_expand_less_black_24px);
holder.tvDescription.setVisibility(View.VISIBLE);
}else{
holder.btnDetail.setBackgroundResource(R.drawable.ic_expand_more_black_24px);
holder.tvDescription.setVisibility(View.GONE);
}
holder.btnDetail.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (holder.tvDescription.getVisibility() == View.GONE) {
holder.btnDetail.setBackgroundResource(R.drawable.ic_expand_less_black_24px);
holder.tvDescription.setVisibility(View.VISIBLE);
mSparseBooleanArray.put(Integer.parseInt(itHomeItem.getNewsid()),true);
} else {
holder.btnDetail.setBackgroundResource(R.drawable.ic_expand_more_black_24px);
holder.tvDescription.setVisibility(View.GONE);
mSparseBooleanArray.put(Integer.parseInt(itHomeItem.getNewsid()),false);
}
}
});

删除缓存目录cache下的文件及文件夹

public static boolean deleteDir(File dir) {
if (dir != null && dir.isDirectory()) {
String[] children = dir.list();
for (String aChildren : children) {
boolean success = deleteDir(new File(dir, aChildren));
if (!success) {
return false;
}
}
}
assert dir != null;
return dir.delete();
}

获取缓存路径下文件大小

public static long getFolderSize(File file) {
long size = 0;
try {
File[] fileList = file.listFiles();
for (File aFileList : fileList) {
// 如果下面还有文件
if (aFileList.isDirectory()) {
size = size + getFolderSize(aFileList);
} else {
size = size + aFileList.length();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return size;
}

缓存显示处理

public static String getCacheSize(File file) {
return getFormatSize(getFolderSize(file));
}
public static String getFormatSize(double size) {
double kiloByte = size / 1024;
if (kiloByte < 1) {
// return size + "Byte";
return "0K";
}

double megaByte = kiloByte / 1024;
if (megaByte < 1) {
BigDecimal result1 = new BigDecimal(Double.toString(kiloByte));
return result1.setScale(2, BigDecimal.ROUND_HALF_UP)
.toPlainString() + "KB";
}

double gigaByte = megaByte / 1024;
if (gigaByte < 1) {
BigDecimal result2 = new BigDecimal(Double.toString(megaByte));
return result2.setScale(2, BigDecimal.ROUND_HALF_UP)
.toPlainString() + "MB";
}

double teraBytes = gigaByte / 1024;
if (teraBytes < 1) {
BigDecimal result3 = new BigDecimal(Double.toString(gigaByte));
return result3.setScale(2, BigDecimal.ROUND_HALF_UP)
.toPlainString() + "GB";
}
BigDecimal result4 = new BigDecimal(teraBytes);
return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString()
+ "TB";
}

BigDecimal使用

BigDecimal.setScale用于格式化小数点 
setScale(1)表示保留以为小数,默认用四舍五入方式
setScale(1,BigDecimal.ROUND_DOWN)直接删除多余的小数位,如2.35会变成2.3
setScale(1,BigDecimal.ROUND_UP)进位处理,2.35变成2.4
setScale(1,BigDecimal.ROUND_HALF_UP)四舍五入,2.35变成2.4setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入,2.35变成2.3,如果是5则向下舍

OkHttp结合Retrofit实现缓存

我们拿一个请求来看

public class GuokrRequest {

private GuokrRequest() {}

private static final String CACHE_CONTROL = "Cache-Control";//请求头,指定请求和响应遵循的缓存机制
private static final Object monitor = new Object();//锁对象
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
if (NetWorkUtil.isNetWorkAvailable(MicroApplication.getContext())) {
int maxAge = 60; // 在线缓存在1分钟内可读取
return originalResponse.newBuilder()
//Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache。
// 在HTTP/1.1协议中,它的含义和Cache- Control:no-cache相同。
.removeHeader("Pragma")
.removeHeader(CACHE_CONTROL)//remove掉其他的缓存头,避免服务端进行一些限制,导致客户端不能进行缓存
//public:告知任何途径的缓存者,可以无条件的缓存该响应.
.header(CACHE_CONTROL, "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 24 * 28; // 离线时缓存保存4周
return originalResponse.newBuilder()
.removeHeader("Pragma")
.removeHeader(CACHE_CONTROL)
//only-if-cached:告知缓存者,我希望内容来自缓存,我并不关心被缓存响应,是否是新鲜的
//max-stale:意思是,我允许缓存者,发送一个,过期不超过指定秒数的,陈旧的缓存.
.header(CACHE_CONTROL, "public, only-if-cached, max-stale=" + maxStale)
.build();
}
}
};

private static File httpCacheDirectory = new File(MicroApplication.getContext().getCacheDir(), "guokrCache");
private static int cacheSize = 10 * 1024 * 1024; // 10 MiB
private static Cache cache = new Cache(httpCacheDirectory, cacheSize);

private static OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)//保证1分钟内走缓存
.addInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)//超过1分钟,读取离线缓存
.cache(cache)
.build();

private static GuokrApi guokrApi = null;

public static GuokrApi getGuokrApi() {
synchronized (monitor) {
if (guokrApi == null) {
guokrApi = new Retrofit.Builder()
.baseUrl("http://www.guokr.com")
.client(client)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(GuokrApi.class);
}
return guokrApi;
}
}
}

然后给出一个学习的链接
​Retrofit2.0使用总结及注意事项​​


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  b1UHV4WKBb2S   2023年11月13日   40   0   0 ide抗锯齿
  iD7FikcuyaVi   2023年11月30日   26   0   0 MacWindowsandroid
  b1UHV4WKBb2S   2023年11月13日   34   0   0 裁剪ideflutter
  zSWNgACtCQuP   2023年11月13日   32   0   0 ide
mrMLeSzj1mp0