注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Android日记之View的绘制流程(一)

前言 View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK...
继续阅读 »

前言


View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK为为Android 9.0版本。



Activity的构成


在了解绘制流程之前,我们首先要了解Activity的构成,我们都知道Activity要用setcontentView()来加载布局,但是这个方法具体是怎么实现的呢,如下所示:


public void setContentView(@LayoutRes int layoutResID) {


getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

public Window getWindow() {
return mWindow;
}

这里有一个getWindow(),返回的是一个mWindow,那这个是什么呢,我们接在在Activity原来里的attach()方法里可以看到如下代码:


final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);

......
}

我们发现了原来mWindow实例化了PhoneWindow,那意思就是说Activity通过setcontentView()加载布局的方法其实就是调用了PhoneWindow的setcontentView()方法,那我们接着往下看PhoneWindow类里面具体是怎么样的。



@Overridepublic void setContentView(int layoutResID) {

// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor(); //在这里
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

点进PhoneWindow这个类我们可以知道这里的PhoneWindow是继承Window的,Window是一个抽象类。然后我们看里面的一些关键地方,比如installDecor()方法,我们看看里面具体做了啥。


private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1); //注释1
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor); //注释2

......
}
}

里面有一个标记注释为1的generateDecor()方法,我们看看具体做了啥。

protected DecorView generateDecor(int featureId) {


// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

发现创建了一个DecorView,而这个DecorView继承了Framelayout,DecorView就是Activity的根View,接着我们回到标记注释2的generateLayout()方法,看看做了啥。

    

protected ViewGroup generateLayout(DecorView decor) {


......

// Inflate the window decor.
//根据不同情况加载不同的布局给layoutResource

int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title; //在这里!!!!!!!
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}

......

mDecor.finishChanging();

return contentParent;
}

generateLayout()方法代码比较长,这里截取了一部分,主要内容就是根据不同的情况加载不同的布局给layoutresource,在这里面有一个基础的XML,就是R.layout.screen_title。这个XMl文件里面有一个ViewStub和两个FrameLayout,ViewStub主要是显示Actionbar的,两个FarmeLayout分别显示TitleView和ContentView,也就是我们的标题和内容。看到这里我们也就知道,一个Activity包含一个Window对象,而Window是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又分为两个区域,分别就是TitleView和ContentView,平常所显示的布局就是在ContentView中。


View的整体绘制流程路口


上一节讲了Activity的构成,最后讲到了DecorView的创建和加载它的资源,但是这个时候DecorView的内容还无法显示,因为还没用加载到Window中,接下来我们看它是怎么被加载到Window中。


还是老样子,DecorView创建完毕的时并且要加载到Window中,我们还是要先了解Activity的创建流程,当我们使用Activit的startActivity()方法时,最终就是调用ActivityThread的handleLaunchActivity方法来创建Activity。而绘制会从根视图ViewRootImpl的performTraversals()方法开始从上到下遍历整个视图树,每个 View 控制负责绘制自己,而ViewGroup还需要负责通知自己的子 View进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。


ViewRootImpl是WindowManager和DecorView的纽带,View的三大流程均是通过这里来完成的,在ActivityThread中,当Activity对象被创建,会将DecorView加入到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。


刚刚也说过了,View的绘制流程是从ViewRootImpl类的performTraversals()方法开始的,它会依次调用performMeasure()performLayout()performDraw()三个方法。其中在performTraversals()调用measure()方法,在measure()方法又会调用onMeasure()方法,在onMeasure()方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了依次measure过程,接着子元素会重复父容器的measure过程,如此返回完成了整个View数的遍历。



理解MeasureSpec


为了理解View的测量过程,我们还需理解MeasureSpec,它在很大程度上决定了一个View的尺寸规格,而且也参与到了View的measure过程中。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个measureSpec来测量出View的宽和高。


MeasureSpec是View的内部类,它代表哦啦一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小,如以下代码所示:


public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

......

public static final int UNSPECIFIED = 0 << MODE_SHIFT;

public static final int EXACTLY = 1 << MODE_SHIFT;

public static final int AT_MOST = 2 << MODE_SHIFT;

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode
int mode)
{
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}


@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}


public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}


......
}

MeasureSpec通过SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,而也可以通过解包得到SpecMode和SpecSize。这里说一下打包成的MeasureSpec是一个int值,不是这个类本身。


对于SpecMode,有三种类型:



  • UPSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。


  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。


  • AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。


Android日记之View的绘制流程(二)

作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

OpenGL中的图片渲染流程

在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据传递数据的三...
继续阅读 »

在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?

我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据


传递数据的三种方式

1.传递数据处理的流程:顶点着色器--->光栅化/图元装配--->片元着色器--->渲染完成

2.TextureData(纹理)、Uniforms:可以直接的传递到顶点着色器或者片元着色器中。
    Attributes(属性):只能传递到顶点着色器中,进过处理后的数据可以传递到片元着色器中

3.着色器中的是我们可以控制的,但是光栅化/图元装配是由系统来完成的,不可控

三种传输方式详解

1.Attributes:只能传递到顶点着色器中,通过处理可以传递到片元着色器中
   使用场景:当数据不停的进行变换时
   经常传递的数据:颜色数据、顶点数据、纹理坐标、关照法线

2.Uniform:可以直接传递数据到顶点/片元着色器中
   使用场景:比较统一的处理方式,不会发生太多变化时
   顶点着色器处理的场景:图形的旋转操作,每个顶点乘以旋转矩阵来完成(旋转矩阵基本是不怎么改变的)
   片元着色器处理场景:处理视频,视频解码之后是由一帧帧的图片来组成的,对视频的颜色空间进行渲染处理(在视频中常使用的颜色空间为YUV)将YUV颜色空间乘以矩阵转化为RGB颜色来处理视频

3.TextureData(纹理):颜色的填充,视频的处理。一般这里的数据不会传递到顶点着色器的,这里主要是用来处理图片的,一般不涉及到顶点数据的处理

收起阅读 »

Android日记之线程池

前言 在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池...
继续阅读 »

前言


在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池来对线程进行管理。在Java 1.5中提供了Executor框架用于把任务的提交和执行解耦。任务的提交交给RUnnable或者Callable,而Executor框架用来处理任务。Executor框架中最核心的成员就是ThreadPoolExecutor,它是线程池的核心实现类。本篇文章就着重讲解ThreadPoolExecutor。


ThreadPoolExecutor介绍


可以通过ThreadPoolExecutor开创建一个线程池,ThreadPoolExecutor类一共有四个构造方法。下面展示的都是拥有最多参数的的构造方法。



public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}


  • corePoolSize:核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则会创建新线程来处理任务;如果等于或者多于corePoolSize,则不会创建,如果调用线程池的prestartAllcoreThread()方法,线程池会提前创建并启动所有核心线程来等待任务。


  • maximumPoolSize:线程池允许创建的最大线程数,如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。


  • keepAliveTime:非核心线程闲置的超时时间,超过这个事件则回收,如果任务很多,并且每个任务的执行事件很短,则可以调用keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上。


  • TimeUnit:keepAliveTime参数的时间单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、秒(SECONDS)、毫秒(MILLOSECONDS)等。


  • BlockingQueue任务队列,如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockiingQueue类型,也就是阻塞队列。


  • ThreadFactory:线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置参数。


  • RejectedExecutionHandler :饱和策略,这是当任务队列中和线程池都满了时所采取的对应策略,默认是ABordPolicy,表示无法处理新任务,并抛出RejetctedExecutionException异常。此外还有3种策略,它们分别如下:



(1)CallerRunsPolicy:用调用者所在的线程来处理任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

(2)DiscardPolicy:不能执行的任务,并将该任务删除。

(3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。


ThreadPoolExecutor的基本使用

package com.ju.executordemo;


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity{


private Button btnStart;
private final int CORE_POOL_SIZE = 4;//核心线程数
private final int MAX_POOL_SIZE = 5;//最大线程数
private final long KEEP_ALIVE_TIME = 10;//空闲线程超时时间
private ThreadPoolExecutor executorPool;
private int songIndex = 0;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
//创建线程池
initExec();
}

private void initView() {
btnStart = findViewById(R.id.btn_start);
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
begin();
}
});
}


public void begin() {
songIndex++;
try {
executorPool.execute(new WorkerThread("歌曲" + songIndex));
} catch (Exception e) {
Log.e("threadtest", "AbortPolicy...已超出规定的线程数量,不能再增加了....");
}

// 所有任务已经执行完毕,我们在监听一下相关数据
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(20 * 1000);
} catch (Exception e) {

}
sout("monitor after");
}
}).start();

}

private void sout(String msg) {
Log.i("threadtest", "monitor " + msg
+ " CorePoolSize:" + executorPool.getCorePoolSize()
+ " PoolSize:" + executorPool.getPoolSize()
+ " MaximumPoolSize:" + executorPool.getMaximumPoolSize()
+ " ActiveCount:" + executorPool.getActiveCount()
+ " TaskCount:" + executorPool.getTaskCount()
);
}



private void initExec() {
executorPool = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingDeque(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}

class WorkerThread implements Runnable {

private String threadName;

public WorkerThread (String name){
threadName = name;
}


@Override
public void run() {
boolean flag = true;
try {
while (flag){
String tn = Thread.currentThread().getName();
//模拟耗时操作
Random random = new Random();
long time = (random.nextInt(5) + 1) * 1000;
Thread.sleep(time);
Log.e("threadtest","线程\"" + tn + "\"耗时了(" + time / 1000 + "秒)下载了第<" + threadName + ">");
//下载完毕跳出循环
flag = false;
}
}catch (Exception e){
e.printStackTrace();
}
}
}

}


上述代码模拟一个下载音乐的例子来演示ThreadPoolExecutor的基本使用,启动ThreadPoolExecutor的函数是execute()方法,然后他需要一个Runnable的参数来进行启动。



ThreadPoolExecutor的其它种类


通过直接或者间接地配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor,其中有 4 种线程池比较常用,它们分别是 FixedThreadPool、CachedThreadPool、SingleThreadExecutor和 ScheduledThreadPool。下面分别介绍这4种线程池。



  • FixedThreadPool


FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建FixedThreadPool的方法, 如下所示:


public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}

FixedThreadPool的corePoolSize和maximumPoolSize都设置为创建FixedThreadPool指定的参数nThreads,也就意味着FixedThreadPool只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime设置为0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以keepAliveTime是无效的参数。另外,任 务队列采用了无界的阻塞队列LinkedBlockingQueue。



当执行execute()方法时,如果当前运行的线程未达到corePoolSize(核心线程数)时 就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到LinkedBlockingQueue中。 FixedThreadPool就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过corePoolSize时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行




  • CachedThreadPool


CachedThreadPool是一个根据需要创建线程的线程池,创建CachedThreadPool的代码如下所示:


public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}

CachedThreadPool的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,这意味着 CachedThreadPool没有核心线程,非核心线程是无界的。keepAliveTime设置为60L,则空闲线程等待新任务 的最长时间为 60s。在此用了阻塞队列 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作 必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。



当执行execute()方法时,首先会执行SynchronousQueue的offer()方法来提交任务,并且查询线程池中是否有空闲的线程执行SynchronousQueue的poll()方法来移除任务。如果有则配对成功,将任务交给这个空闲的线程处理;如果没有则配对失败,创建新的线程去处理任务。当线程池中的线程空闲时,它会执行 SynchronousQueue的poll()方法,等待SynchronousQueue中新提交的任务。如果超过 60s 没有新任务提交到 SynchronousQueue,则这个空闲线程将终止。因为maximumPoolSize 是无界的,所以如果提交的任务大于线 程池中线程处理任务的速度就会不断地创建新线程。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务。




  • SingleThreadExecutor


SingleThreadExecutor是使用单个工作线程的线程池,其创建源码如下所示:


public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}

corePoolSize和maximumPoolSize都为1,意味着SingleThreadExecutor只有一个核心线程,其他的参数都 和FixedThreadPool一样,这里就不赘述了。SingleThreadExecutor的execute()方法的执行示意图如图5所示。


当执行execute()方法时,如果当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务。如果当前有运行的线程,则将任务添加到阻塞队列LinkedBlockingQueue中。因此,SingleThreadExecutor能确保所有的任务在一个线程中按照顺序逐一执行。




  • ScheduledThreadPool


ScheduledThreadPool是一个能实现定时和周期性任务的线程池,它的创建源码如下所示



static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);

}

这里创建了ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下所示:

public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}

从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是ThreadPoolExecutor的 构造方法。corePoolSize是传进来的固定数值,maximumPoolSize的值是Integer.MAX_VALUE。因为采用的 DelayedWorkQueue是无界的,所以maximumPoolSize这个参数是无效的。


当执行ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者scheduleWithFixedDelay()方法时,会向DelayedWorkQueue添加一个 实现RunnableScheduledFuture接口的ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到corePoolSize。如果没有则新建线程并启动它,但并不是立即去执行任务,而是去DelayedWorkQueue中取ScheduledFutureTask,然后去执行任务。如果运行的线程达到了corePoolSize时,则将任务添加到DelayedWorkQueue中。DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask中time变量改为下次要执行的时间并放回到DelayedWorkQueue中。


作者:居居居居居居x
链接:https://www.jianshu.com/p/70a8d302fa0f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

安卓选择器类库-AndroidPicker

UI
示例图:AndroidPicker安卓选择器类库,包括日期及时间选择器(可设置范围)、单项选择器(可用于性别、职业、学历、星座等)、城市地址选择器(分省级、地级及县级)、数字选择器(可用于年龄、身高、体重、温度等)、双项选择器、颜色选择器、文件及目录选择器等…...
继续阅读 »

示例图:



AndroidPicker

安卓选择器类库,包括日期及时间选择器(可设置范围)、单项选择器(可用于性别、职业、学历、星座等)、城市地址选择器(分省级、地级及县级)、数字选择器(可用于年龄、身高、体重、温度等)、双项选择器、颜色选择器、文件及目录选择器等……

Install

“app”是测试用例;“library”包括WheelPicker、ColorPicker、FilePicker、MultiplePicker, WheelPicker包括DatePicker、TimePicker、OptionPicker、LinkagePicker、AddressPicker、NumberPicker、DoublePicker等。 其中WheelPicker、FilePicker及ColorPicker是独立的,需要用哪个就只依赖哪个,

 具体步骤如下: 第一步,在项目根目录下的build.gradle里加:

repositories {
maven {
url "https://jitpack.io"
}
}

第二步,在项目的app模块下的build.gradle里加: 滚轮选择器:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:版本号'
}

文件目录选择器:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:FilePicker:版本号'
}

颜色选择器:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:ColorPicker:版本号'
}

注:Support版本截止1.5.6,从2.0.0开始为AndroidX版本。

Support版本依赖:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:1.5.6.20181018'
}

AndroidX版本依赖:

dependencies {
implementation 'com.github.gzu-liyujiang.AndroidPicker:Common:2.0.0'
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:2.0.0'
}

ProGuard

由于地址选择器使用了fastjson来解析,混淆时候需要加入以下类似的规则,不混淆Province、City等实体类。

-keepattributes InnerClasses,Signature
-keepattributes *Annotation*

-keep class cn.qqtheme.framework.entity.** { *;}

Sample (更多用法详见示例项目)

各种设置方法:

picker.setXXX(...);

如:
设置选项偏移量,可用来要设置显示的条目数,范围为1-5,1显示3行、2显示5行、3显示7行……

picker.setOffset(...);

设置启用循环

picker.setCycleDisable(false);

设置每项的高度,范围为2-4

picker.setLineSpaceMultiplier(...);
picker.setItemHeight(...);

设置文字颜色、字号、字体等

picker.setTextColor(...);
picker.setTextSize(...);
picker.setTextPadding(...);
picker.setTextSizeAutoFit(...);
picker.setTypeface(...);

设置单位标签

picker.setLabel(...);
picker.setOnlyShowCenterLabel(...))

设置默认选中项

picker.setSelectedItem(...);
picker.setSelectedIndex(...);

设置滚轮项填充宽度,分割线最长

picker.setUseWeight(true);
picker.setDividerRatio(WheelView.DividerConfig.FILL);

设置触摸弹窗外面是否自动关闭

picker.setCanceledOnTouchOutside(...);

设置分隔线配置项,设置null将隐藏分割线及阴影

picker.setDividerConfig(...);
picker.setDividerColor(...);
picker.setDividerRatio(...);
picker.setDividerVisible(...);

设置内容边距

picker.setContentPadding(...);

设置选中项背景色

picker.setShadowColor(...)

自定义顶部及底部视图

picker.setHeaderView(...);
picker.setFooterView(...);

获得内容视图(不要调用picker.show()方法),可以将其加入到其他容器视图(如自定义的Dialog的视图)中

picker.getContentView();

获得按钮视图(需要先调用picker.show()方法),可以调用该视图相关方法,如setVisibility()

picker.getCancelButton();
picker.getSubmitButton();

自定义选择器示例:

        CustomHeaderAndFooterPicker picker = new CustomHeaderAndFooterPicker(this);
picker.setOnOptionPickListener(new OptionPicker.OnOptionPickListener() {
@Override
public void onOptionPicked(int position, String option) {
showToast(option);
}
});
picker.show();

核心滚轮控件为WheelView,可以参照SinglePicker、DateTimePicker及LinkagePicker自行扩展。


代码下载:AndroidPicker-master.zip

原文链接:https://github.com/gzu-liyujiang/AndroidPicker

收起阅读 »

揭开 LiveData 的通知机制的神秘面纱

LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 ...
继续阅读 »

LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 LiveData 的通知机制是它们的主要责任。所以,就这个机会我们也来分析一下 LiveData 通知的实现过程。

1、一个 LiveData 的问题

LiveData的问题
有两个页面 A 和 B,A 是一个 Fragment ,是一个列表的展示页;B 是其他的页面。首先,A 会更新页面,并且为了防止连续更新,在每次更新之前需要检查一个布尔值,只有为 false 的时候才允许从网络加载数据。每次加载数据之前会将该布尔值置为 true,拿到了结果之后置为 false. 这里拿到的结果是借助 LiveData 来通知给页面进行更新的。

现在,A 打开了 B,B 中对列表中的数据进行了更新,然后发了一条类似于广播的消息。此时,A 接收了消息并进行数据加载。过了一段时间,B 准备退出,再退出的时候又对列表中的项目进行了更新,所以此时又发出了一条消息。

B 关闭了,我们回到了 A 页面。但是,此时,我们发现 A 页面中的数据只包含了第一次的数据更新,第二次的数据更新没有体现在列表中。

用代码来描述的话大致是下面这样,

//  A
public class A extends Fragment {

private boolean loading = false;

private MyViewModel vm;

// ......

/**
* Register load observer.
*/
public void registerObservers() {
vm.getData().observe(this, resources -> {
loading = false;
// ... show in list
})
}

/**
* Load data from server.
*/
public void loadData() {
if (loading) return;
loading = true;
vm.load();
}

/**
* On receive message.
*/
public void onReceive() {
loadData();
}
}

public class B extends Activity {

public void doBusiness1() {
sendMessage(MSG); // Send message when on foreground.
}

@Override
public void onBackpressed() {
// ....
sendMessage(MSG); // Send message when back
}
}

public class MyViewModel extends ViewModel {

private MutableLiveData> data;

public MutableLiveData> getData() {
if (data == null) {
data = new MutableLiveData<>();
}
return data;
}

public void load() {
Object result = AsyncGetData.getData(); // Get data
if (data != null) {
data.setValue(Resouces.success(result));
}
}
}


A 打开了 B 之后,A 处于后台,B 处于前台。此时,B 调用 doBusiness1() 发送了一条消息 MSG,A 中在 onReceive() 中收到消息,并调用 loadData() 加载数据。然后,B 处理完了业务,准备退出的时候发现其他数据发生了变化,所以又发了一条消息,然后 onReceive() 中收到消息,并调用 loadData(). 但此时发现 loading 为 true. 所以,我们后来对数据的修改没有体现到列表上面。

2、问题的原因

如果用上面的示例代码作为例子,那么出现问题的原因就是当 A 处于后台的时候。虽然调用了 loadData() 并且从网络中拿到了数据,但是调用 data.setValue() 方法的时候无法通知到 A 中。所以,loading = false 这一行无法被调用到。第二次发出通知的时候,一样调用到了 loadData(),但是因为此时 loading 为 true,所以并没有执行加载数据的操作。而当从 B 中完全回到 A 的时候,第一次加载的数据被 A 接收到。所以,列表中的数据是第一次加载时的数据,第二次加载事件丢失了。

解决这个问题的方法当然比较简单,可以当接收到事件的时候使用布尔变量监听,然后回到页面的时候发现数据发生变化再执行数据加载:

// 类 A
public class A extends Fragment {

private boolean dataChanged;

/**
* On receive message.
*/
public void onReceive() {
dataChanged = true;
}

@Override
public void onResume() {
// ...
if (dataChanged) {
loadData();
}
}
}


对于上面的问题,当我们调用了 setValue() 之后将调用到 LiveData 类的 setValue() 方法,

@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}


这里表明该方法必须在主线程中被调用,最终事件的分发将会交给 dispatchingValue() 方法来执行:

private void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
// 发送事件
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}


然后,会调用 considerNotify() 方法来最终将事件传递出去,

private void considerNotify(ObserverWrapper observer) {
// 这里会因为当前的 Fragment 没有处于 active 状态而退出方法
if (!observer.mActive) {
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}


这里会因为当前的 Fragment 没有处于 active 状态而退出 considerNotify() 方法,从而消息无法被传递出去。

3、LiveData 的通知机制

LiveData 的通知机制并不复杂,它的类主要包含在 livedata-core 包下面,总共也就 3 个类。LiveData 是一个抽象类,它有一个默认的实现就是 MutableLiveData.

LiveData 主要依靠内部的变量 mObservers 来缓存订阅的对象和订阅信息。其定义如下,使用了一个哈希表进行缓存和映射,

private SafeIterableMap, ObserverWrapper> mObservers = new SafeIterableMap<>();
每当我们调用一次 observe() 方法的时候就会有一个映射关系被加入到哈希表中,

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// 持有者当前处于被销毁状态,因此可以忽略此次观察
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}


从上面的代码我们可以看出,添加到映射关系中的类会先被包装成 LifecycleBoundObserver 对象。然后使用该对象对 owner 的生命周期进行监听。

这的 LifecycleBoundObserver 和 ObserverWrapper 两个类的定义如下,

class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
@NonNull final LifecycleOwner mOwner;

LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer observer) {
super(observer);
mOwner = owner;
}

@Override
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}

@Override
boolean isAttachedTo(LifecycleOwner owner) {
return mOwner == owner;
}

@Override
void detachObserver() {
mOwner.getLifecycle().removeObserver(this);
}
}

private abstract class ObserverWrapper {
final Observer mObserver;
boolean mActive;
int mLastVersion = START_VERSION;

ObserverWrapper(Observer observer) {
mObserver = observer;
}

abstract boolean shouldBeActive();

boolean isAttachedTo(LifecycleOwner owner) {
return false;
}

void detachObserver() {}

void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
dispatchingValue(this);
}
}
}


上面的类中我们先来关注 LifecycleBoundObserver 中的 onStateChanged() 方法。该方法继承自 LifecycleObserver. 这里的 Lifecycle.Event 是一个枚举类型,定义了一些与生命周期相关的枚举值。所以,当 Activity 或者 Fragment 的生命周期发生变化的时候会回调这个方法。从上面我们也可以看出,该方法内部又调用了基类的 activeStateChanged() 方法,该方法主要用来更新当前的 Observer 是否处于 Active 的状态。我们上面无法通知也是因为在这个方法中 mActive 被置为 false 造成的。

继续看 activeStateChanged() 方法,我们可以看出在最后的几行中,它调用了 dispatchingValue(this) 方法。所以,当 Fragment 从处于后台切换到前台之后,会将当前缓存的值通知给观察者。

那么值是如何缓存的,以及缓存了多少值呢?回到之前的 setValue() 和 dispatchingValue() 方法中,我们发现值是以一个单独的变量进行缓存的,

private volatile Object mData = NOT_SET;
因此,在我们的示例中,当页面从后台切换到前台的时候,只能将最后一次缓存的结果通知给观察者就真相大白了。

总结

从上面的分析中,我们对 LiveData 总结如下,

当调用 observe() 方法的时候,我们的观察者将会和 LifecycleOwner (Fragment 或者 Activity) 一起被包装到一个类中,并使用哈希表建立映射关系。同时,还会对 Fragment 或者 Activity 的生命周期方法进行监听,以此来达到监听观察者是否处于 active 状态的目的。

当 Fragment 或者 Activity 处于后台的时候,其内部的观察者将处于非 active 状态,此时使用 setValue() 设置的值会缓存到 LiveData 中。但是这种缓存只能缓存一个值,新的值会替换旧的值。因此,当页面从后台恢复到前台的时候只有最后设置的一个值会被传递给观察者。

在 2 中的当 Fragment 或者 Activity 从后台恢复的时候进行通知也是通过监听其生命周期方法实现的。

调用了 observe() 之后,Fragment 或者 Activity 被缓存了起来,不会造成内存泄漏吗?答案是不会的。因为 LiveData 可以对其生命周期进行监听,当其处于销毁状态的时候,该映射关系将被从缓存中移除。

通俗地讲,

LiveData 里面保存了一个值,当 Fragment 处于后台更新这个值的时候,会直接把这个值改掉,但是不会通知到观察者,直到 Fragment 从后台回来的时候才通知给观察者。而这里 LiveData 感知 Fragment 处于后台还是前台是依靠 LifecycleObserver 的通知机制来完成的。

收起阅读 »

揭开 ViewModel 的生命周期控制的神秘面纱

1、从一个 Bug 说起想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。起因是这样的,昨天在...
继续阅读 »

1、从一个 Bug 说起

想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。

起因是这样的,昨天在修复程序中的 Bug 的时候遇到了一个异常,是从 ViewModel 中获取存储的数据的时候报了空指针。我启用了开发者模式的 “不保留活动” 之后很容易地重现了这个异常。出现错误的原因也很简单,相关的代码如下:

private ReceiptViewerViewModel viewModel;

@Override
protected void doCreateView(Bundle savedInstanceState) {
viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1
handleIntent(savedInstanceState);
// ...
}

private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}


在方法 doCreateView() 中我获取了 viewModel 实例,然后在 handleIntent() 方法中从 Intent 中取出传入的参数。当然,还要使用 viewModel 的 getter 方法从其中取出 loadingStatus 并使用。在使用的时候抛了空指针。

显然,一般情况下是不会出现问题的,但是如果 Activity 在后台被销毁了,那么再重建的时候就会出现空指针异常。

解决方法也比较简单,在 onSaveInstanceState() 方法中将数据缓存起来即可,即:

private void handleIntent(Bundle savedInstanceState) {
LoadingStatus loadingStatus;
if (savedInstanceState == null) {
loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
} else {
loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);
}
viewModel.setLoadingStatus(loadingStatus);
}

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());
}


现在的问题是 ViewModel 的生命周期问题,有人说在 doCreateView() 方法的 1 处得到的不是之前的 ViewModel 吗,数据不是之前已经设置过了吗?所以,这牵扯 ViewModel 是在什么时候被销毁和重建的问题。

2、ViewModel 的生命周期

有的人希望使用 ViewModel 缓存 Activity 的信息,然后在 doCreateView() 方法的 1 处得到之前的 ViewModel 实例,这样 ViewModel 的数据就是 Activity 销毁之前的数据,这可行吗?我们从源码角度来看下这个问题。

首先,每次获取 viewmodel 实例的时候都会调用下面的方法来获取 ViewModel 实例。从下面的 get() 方法中可以看出,实例化过的 ViewModel 是从 mViewModelStore 中获取的。如果由 ViewModelStores.of(activity) 方法得到的 mViewModelStore 不是同一个,那么得到的 ViewModel 也不是同一个。

下面方法中的 get() 方法中后续的逻辑是如果之前没有缓存过 ViewModel,那么就构建一个新的实例并将其放进 mViewModelStore 中。这部分代码逻辑比较简单,我们不继续分析了。

// ViewModelProviders#of()
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
ViewModelProvider.AndroidViewModelFactory factory =
ViewModelProvider.AndroidViewModelFactory.getInstance(activity);
return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1
}

// ViewModelProvider#get()
public T get(@NonNull String key, @NonNull Class modelClass) {
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}

viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}


我们回到上述 of() 方法的 1 处,来看下 ViewModelStores.of() 方法,其定义如下:

// ViewModelStores#of()
public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
return ((ViewModelStoreOwner) activity).getViewModelStore();
}
return holderFragmentFor(activity).getViewModelStore();
}

// HolderFragment#holderFragmentFor()
public static HolderFragment holderFragmentFor(FragmentActivity activity) {
return sHolderFragmentManager.holderFragmentFor(activity);
}


这里会从 holderFragmentFor() 方法中获取一个 HolderFragment 实例,它是一个 Fragment 的实现类。然后从该实例中获取 ViewModelStore 的实例。所以,ViewModel 对生命周期的管理与 Glide 和 RxPermission 等框架的处理方式一致,就是使用一个空的 Fragment 来进行生命周期管理。

对于 HolderFragment,其定义如下。从下面的代码我们可以看出,上述用到的 ViewModelStore 实例就是 HolderFragment 的一个局部变量。所以,ViewModel 使用空的 Fragment 管理生命周期实锤了。

public class HolderFragment extends Fragment implements ViewModelStoreOwner {
private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
private ViewModelStore mViewModelStore = new ViewModelStore();

public HolderFragment() {
setRetainInstance(true);
}

// ...
}


此外,我们注意到上面的 HolderFragment 的构造方法中还调用了 setRetainInstance(true) 这一行代码。我们进入该方法看它的注释:

Control whether a fragment instance is retained across Activityre-creation (such as from a configuration change). This can onlybe used with fragments not in the back stack. If set, the fragmentlifecycle will be slightly different when an activity is recreated:

就是说,当 Activity 被重建的时候该 Fragment 会被保留,然后传递给新创建的 Activity. 但是,这只适用于不处于后台的 Fragment. 所以,如果 Activity 处于后台的时候,Fragment 不会保留,那么它得到的 ViewModelStore 实例就不同了。

所以,总结下来,准确地讲:当 Activity 处于前台的时候被销毁了,那么得到的 ViewModel 是之前实例过的 ViewModel;如果 Activity 处于后台时被销毁了,那么得到的 ViewModel 不是同一个。举例说,如果 Activity 因为配置发生变化而被重建了,那么当重建的时候,ViewModel 是之前的实例;如果因为长期处于后台而被销毁了,那么重建的时候,ViewModel 就不是之前的实例了。


回到之前的 holderFragmentFor() 方法,我们看下这里具体做了什么,其定义如下。

// HolderFragmentManager#holderFragmentFor()
HolderFragment holderFragmentFor(FragmentActivity activity) {
// 使用 FragmentManager 获取 HolderFragment
FragmentManager fm = activity.getSupportFragmentManager();
HolderFragment holder = findHolderFragment(fm);
if (holder != null) {
return holder;
}
// 从哈希表中获取 HolderFragment
holder = mNotCommittedActivityHolders.get(activity);
if (holder != null) {
return holder;
}

if (!mActivityCallbacksIsAdded) {
mActivityCallbacksIsAdded = true;
activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
}
holder = createHolderFragment(fm);
// 将新的实例放进哈希表中
mNotCommittedActivityHolders.put(activity, holder);
return holder;
}


首先,尝试使用 FragmentManager 来获取 HolderFragment,如果获取不到就从 mNotCommittedActivityHolders 中进行获取。这里的 mNotCommittedActivityHolders 是一个哈希表,每次实例化的新的 HolderFragment 会被添加到哈希表中。

另外,上面的方法中还使用了 ActivityLifecycleCallbacks 对 Activity 的生命周期进行监听。其定义如下,

private ActivityLifecycleCallbacks mActivityCallbacks =
new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
}
};


当 Activity 被销毁的时候会从哈希表中移除映射关系。所以,每次 Activity 被销毁的时候哈希表中的映射关系都不存在了。而之所以 ViewModel 能够实现在 Activity 配置发生变化的时候获取之前的 ViewModel 是通过上面的 setRetainInstance(true) 和 findHolderFragment(fm) 来实现的。

总结
以上就是 ViewModel 的生命周期的总结。我们只是通过对主流程的分析研究了它的生命周期的流程,实际上内部还有许多小细节,逻辑也比较简单,我们就不一一说明了。

viewmodel-lifecycle
这里使用了 Activity rotated,也就是 Activity 处于前台的时候配置发生变化的情况,而不是处于后台,不知道你之前有没有注意这一点呢?

原文链接:揭开 ViewModel 的生命周期控制的神秘面纱

收起阅读 »

一文说透 Android 应用架构 MVC、MVP、MVVM 和 组件化

MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和...
继续阅读 »

MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和前面三种架构设计不是一个层次的。它们之间的关系是,组件化的各个组件可以使用前面三种架构设计。我们只有了解了这些架构设计的特点之后,才能在进行开发的时候选择适合自己项目的架构模式,这也是本文的目的。

1、MVC

MVC (Model-View-Controller, 模型-视图-控制器),标准的 MVC 是这个样子的:

模型层 (Model):业务逻辑对应的数据模型,无 View 无关,而与业务相关;

视图层 (View):一般使用 XML 或者 Java 对界面进行描述;

控制层 (Controllor):在 Android 中通常指 Activity 和 Fragment,或者由其控制的业务类。

Activity 并非标准的 Controller,它一方面用来控制了布局,另一方面还要在 Activity 中写业务代码,造成了 Activity 既像 View 又像Controller。

在 Android 开发中,就是指直接使用 Activity 并在其中写业务逻辑的开发方式。显然,一方面 Activity 本身就是一个视图,另一方面又要负责处理业务逻辑,因此逻辑会比较混乱。

这种开发方式不太适合 Android 开发。

2、MVP

2.1 概念梳理

MVP (Model-View-Presenter) 是 MVC 的演化版本,几个主要部分如下:

模型层 (Model):主要提供数据存取功能。

视图层 (View):处理用户事件和视图。在 Android 中,可能是指 Activity、Fragment 或者 View。

展示层 (Presenter):负责通过 Model 存取数据,连接 View 和 Model,从 Model 中取出数据交给 View。

所以,对于 MVP 的架构设计,我们有以下几点需要说明:

这里的 Model 是用来存取数据的,也就是用来从指定的数据源中获取数据,不要将其理解成 MVC 中的 Model。在 MVC 中 Model 是数据模型,在 MVP 中,我们用 Bean 来表示数据模型。

Model 和 View 不会直接发生关系,它们需要通过 Presenter 来进行交互。在实际的开发中,我们可以用接口来定义一些规范,然后让我们的 View 和 Model 实现它们,并借助 Presenter 进行交互即可。

为了说明 MVP 设计模式,我们给出一个示例程序。你可以在 Android-references 中获取到它的源代码。

2.2 示例程序

在该示例中,我们使用了:

开眼视频的 API 作为数据源;

Retrofit 进行数据访问;

使用 ARouter 进行路由;

使用 MVP 设计模式作为程序架构。



这里我们首先定义了 MVP 模式中的最顶层的 View 和 Presenter,在这里分别是 BaseView 和 BasePresenter,它们在该项目中是两个空的接口,在一些项目中,我们可以根据自己的需求在这两个接口中添加自己需要的方法。

然后,我们定义了 HomeContract。它是一个抽象的接口,相当于一层协议,用来规定指定的功能的 View 和 Presenter 分别应该具有哪些方法。通常,对于不同的功能,我们需要分别实现一个 MVP,每个 MVP 都会有一个对应的 Contract。笔者认为它的好处在于,将指定的 View 和 Presenter 的接口定义在一个接口中,更加集中。它们各自需要实现的方法也一目了然地展现在了我们面前。

这里根据我们的业务场景,该接口的定义如下:

public interface HomeContract {

interface IView extends BaseView {
void setFirstPage(List itemLists);
void setNextPage(List itemLists);
void onError(String msg);
}

interface IPresenter extends BasePresenter {
void requestFirstPage();
void requestNextPage();
}
}


HomeContract 用来规定 View 和 Presenter 应该具有的操作,在这里它用来指定主页的 View 和 Presenter 的方法。从上面我们也可以看出,这里的 IView 和 IPresenter 分别实现了 BaseView 和 BasePresenter。

上面,我们定义了 V 和 P 的规范,MVP 中还有一项 Model,它用来从网络中获取数据。这里我们省去网络相关的具体的代码,你只需要知道 APIRetrofit.getEyepetizerService() 是用来获取 Retrofit 对应的 Service,而 getMoreHomeData() 和 getFirstHomeData() 是用来从指定的接口中获取数据就行。下面是 HomeModel 的定义:

public class HomeModel {

public Observable getFirstHomeData() {
return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());
}

public Observable getMoreHomeData(String url) {
return APIRetrofit.getEyepetizerService().getMoreHomeData(url);
}
}


OK,上面我们已经完成了 Model 的定义和 View 及 Presenter 的规范的定义。下面,我们就需要具体去实现 View 和 Presenter。

首先是 Presenter,下面是我们的 HomePresenter 的定义。在下面的代码中,为了更加清晰地展示其中的逻辑,我删减了一部分无关代码:

public class HomePresenter implements HomeContract.IPresenter {

private HomeContract.IView view;

private HomeModel homeModel;

private String nextPageUrl;

// 传入View并实例化Model
public HomePresenter(HomeContract.IView view) {
this.view = view;
homeModel = new HomeModel();
}



// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调

@Override
public void requestFirstPage() {
Disposable disposable = homeModel.getFirstHomeData()
// ....
.subscribe(itemLists -> { view.setFirstPage(itemLists); },
throwable -> { view.onError(throwable.toString()); });
}



// 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调

@Override
public void requestNextPage() {
Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)
// ....
.subscribe(itemLists -> { view.setFirstPage(itemLists); },
throwable -> { view.onError(throwable.toString()); });
}
}


从上面我们可以看出,在 Presenter 需要将 View 和 Model 建立联系。我们需要在初始化的时候传入 View,并实例化一个 Model。Presenter 通过 Mode l获取数据,并在拿到数据的时候,通过 View 的方法通知给 View 层。

然后,就是我们的 View 层的代码,同样,我对代码做了删减:

@Route(path = BaseConstants.EYEPETIZER_MENU)
public class HomeActivity extends CommonActivity implements HomeContract.IView {

// 实例化Presenter
private HomeContract.IPresenter presenter;
{
presenter = new HomePresenter(this);
}

@Override
protected int getLayoutResId() {
return R.layout.activity_eyepetizer_menu;
}

@Override
protected void doCreateView(Bundle savedInstanceState) {
// ...
// 使用Presenter请求数据
presenter.requestFirstPage();
loading = true;
}

private void configList() {
// ...
getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 请求下一页的数据
presenter.requestNextPage();
}
}
});
}



// 当请求到结果的时候在页面上做处理,展示到页面上

@Override
public void setFirstPage(List itemLists) {
loading = false;
homeAdapter.addData(itemLists);
}



// 当请求到结果的时候在页面上做处理,展示到页面上

@Override
public void setNextPage(List itemLists) {
loading = false;
homeAdapter.addData(itemLists);
}

@Override
public void onError(String msg) {
ToastUtils.makeToast(msg);
}

// ...
}


从上面的代码中我们可以看出实际在 View 中也要维护一个 Presenter 的实例。当需要请求数据的时候会使用该实例的方法来请求数据,所以,在开发的时候,我们需要根据请求数据的情况,在 Presenter 中定义接口方法。

实际上,MVP 的原理就是 View 通过 Presenter 获取数据,获取到数据之后再回调 View 的方法来展示数据。

另外一个值得注意的地方就是,在实际的使用过程中,尤其是进行异步请求的时候,为了给 Model 和 View 之间解耦,我们会在 Presenter 中使用 Handler 收发消息来建立两者之间的桥梁。

2.3 MVC 和 MVP 的区别

MVC 中是允许 Model 和 View 进行交互的,而MVP中,Model 与 View 之间的交互由Presenter完成;

MVP 模式就是将 P 定义成一个接口,然后在每个触发的事件中调用接口的方法来处理,也就是将逻辑放进了 P 中,需要执行某些操作的时候调用 P 的方法就行了。

2.4 MVP的优缺点

优点:

降低耦合度,实现了 Model 和 View 真正的完全分离,可以修改 View 而不影响 Modle;

模块职责划分明显,层次清晰;

隐藏数据;

Presenter 可以复用,一个 Presenter 可以用于多个 View,而不需要更改 Presenter 的逻辑;

利于测试驱动开发,以前的 Android 开发是难以进行单元测试的;

View 可以进行组件化,在 MVP 当中,View 不依赖 Model。

缺点:

Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->View 的手动同步逻辑,造成 Presenter 比较笨重,维护起来会比较困难;

由于对视图的渲染放在了 Presenter 中,所以视图和 Presenter 的交互会过于频繁;

如果 Presenter 过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密,一旦视图需要变更,那么 Presenter 也需要变更了。

3、MVVM (分手大师)

3.1 基础概念

MVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

模型层 (Model):负责从各种数据源中获取数据;

视图层 (View):在 Android 中对应于 Activity 和 Fragment,用于展示给用户和处理用户交互,会驱动 ViewModel 从 Model 中获取数据;

ViewModel 层:用于将 Model 和 View 进行关联,我们可以在 View 中通过 ViewModel 从 Model 中获取数据;当获取到了数据之后,会通过自动绑定,比如 DataBinding,来将结果自动刷新到界面上。

使用 Google 官方的 Android Architecture Components ,我们可以很容易地将 MVVM 应用到我们的应用中。下面,我们就使用它来展示一下 MVVM 的实际的应用。你可以在 Android-references 中获取到它的源代码。

3.2 示例程序

在该项目中,我们使用了:

果壳网的 API 作为数据源;

使用 Retrofit 进行网络数据访问;

使用 ViewMdeol 作为整体的架构设计。

mvvm
这里的 model.data 下面的类是对应于网络的数据实体的,由 JSON 自动生成,这里我们不进行详细描述。这里的 model.repository 下面的两个类是用来从网络中获取数据信息的,我们也忽略它的定义。

上面就是我们的 Model 的定义,并没有太多的内容,基本与 MVP 一致。

下面的是 ViewModel 的代码,我们选择了其中的一个方法来进行说明。当我们定义 ViewModel 的时候,需要继承 ViewModel 类。

public class GuokrViewModel extends ViewModel {

public LiveData> getGuokrNews(int offset, int limit) {
MutableLiveData> result = new MutableLiveData<>();
GuokrRetrofit.getGuokrService().getNews(offset, limit)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onError(Throwable e) {
result.setValue(Resource.error(e.getMessage(), null));
}

@Override
public void onComplete() { }

@Override
public void onSubscribe(Disposable d) { }

@Override
public void onNext(GuokrNews guokrNews) {
result.setValue(Resource.success(guokrNews));
}
});
return result;
}
}


这里的 ViewModel 来自 android.arch.lifecycle.ViewModel,所以,为了使用它,我们还需要加入下面的依赖:

api "android.arch.lifecycle:runtime:$archVersion"
api "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"


在 ViewModel 的定义中,我们直接使用 Retrofit 来从网络中获取数据。然后当获取到数据的时候,我们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层,我们只需要调用该方法,并对返回的 LiveData 进行"监听"即可。这里,我们将错误信息和返回的数据信息进行了封装,并且封装了一个代表当前状态的枚举信息,你可以参考源代码来详细了解下这些内容。

上面我们定义完了 Model 和 ViewModel,下面我们看下 View 层的定义,以及在 View 层中该如何使用 ViewModel。

@Route(path = BaseConstants.GUOKR_NEWS_LIST)
public class NewsListFragment extends CommonFragment {

private GuokrViewModel guokrViewModel;

private int offset = 0;

private final int limit = 20;

private GuokrNewsAdapter adapter;

@Override
protected int getLayoutResId() {
return R.layout.fragment_news_list;
}

@Override
protected void doCreateView(Bundle savedInstanceState) {
// ...

guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);

fetchNews();
}

private void fetchNews() {
guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {
if (guokrNewsResource == null) {
return;
}
switch (guokrNewsResource.status) {
case FAILED:
ToastUtils.makeToast(guokrNewsResource.message);
break;
case SUCCESS:
adapter.addData(guokrNewsResource.data.getResult());
adapter.notifyDataSetChanged();
break;
}
});
}
}


以上就是我们的 View 层的定义,这里我们先使用了

这里的view.fragment包下面的类对应于实际的页面,这里我们 ViewModelProviders 的方法来获取我们需要使用的 ViewModel,然后,我们直接使用该 ViewModel 的方法获取数据,并对返回的结果进行“监听”即可。

以上就是 MVVM 的基本使用,当然,这里我们并没有使用 DataBinding 直接与返回的列表信息进行绑定,它被更多的用在了整个 Fragment 的布局中。

3.3 MVVM 的优点和缺点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

低耦合:视图(View)可以独立于Model变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。

可重用性:你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。

独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

可测试:界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。

4、组件化

4.1 基础概念

所谓的组件化,通俗理解就是将一个工程分成各个模块,各个模块之间相互解耦,可以独立开发并编译成一个独立的 APP 进行调试,然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协作、同步开发;被分割出来的模块又可以在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤其适用于比较大型的项目。

简单了解了组件化之后,让我们来看一下如何实现组件化开发。你可能之前听说过组件化开发,或者被其高大上的称谓吓到了,但它实际应用起来并不复杂,至少借助了现成的框架之后并不复杂。这里我们先梳理一下,在应用组件化的时候需要解决哪些问题:

如何分成各个模块?我们可以根据业务来进行拆分,对于比较大的功能模块可以作为应用的一个模块来使用,但是也应该注意,划分出来的模块不要过多,否则可能会降低编译的速度并且增加维护的难度。

各个模块之间如何进行数据共享和数据通信?我们可以把需要共享的数据划分成一个单独的模块来放置公共数据。各个模块之间的数据通信,我们可以使用阿里的 ARouter 进行页面的跳转,使用封装之后的 RxJava 作为 EventBus 进行全局的数据通信。

如何将各个模块打包成一个独立的 APP 进行调试?首先这个要建立在2的基础上,然后,我们可以在各个模块的 gradle 文件里面配置需要加载的 AndroidManifest.xml 文件,并可以为每个应用配置一个独立的 Application 和启动类。

如何防止资源名冲突问题?遵守命名规约就能规避资源名冲突问题。

如何解决 library 重复依赖以及 sdk 和依赖的第三方版本号控制问题?可以将各个模块公用的依赖的版本配置到 settings.gradle 里面,并且可以建立一个公共的模块来配置所需要的各种依赖。

Talk is cheap,下面让我们动手实践来应用组件化进行开发。你可以在Github中获取到它的源代码。

4.2 组件化实践

1. 包结构

首先,我们先来看整个应用的包的结构。如下图所示,该模块的划分是根据各个模块的功能来决定的。图的右侧白色的部分是各个模块的文件路径,我推荐使用这种方式,而不是将各个模块放置在 app 下面,因为这样看起来更加的清晰。为了达到这个目的,你只需要按照下面的方式在 settings.gralde 里面配置一下各个模块的路径即可。注意在实际应用的时候模块的路径的关系,不要搞错了。

组件化
然后,我们介绍一下这里的 commons 模块。它用来存放公共的资源和一些依赖,这里我们将两者放在了一个模块中以减少模块的数量。下面是它的 gradle 的部分配置。这里我们使用了 api 来引入各个依赖,以便在其他的模块中也能使用这些依赖。

dependencies {
api fileTree(include: ['*.jar'], dir: 'libs')
// ...
// router
api 'com.alibaba:arouter-api:1.3.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
// walle
api 'com.meituan.android.walle:library:1.1.6'
// umeng
api 'com.umeng.sdk:common:1.5.3'
api 'com.umeng.sdk:analytics:7.5.3'
api files('libs/pldroid-player-1.5.0.jar')
}


2. 路由

接着,我们来看一下路由框架的配置。这里,我们使用阿里的 ARouter 来进行页面之间的跳转,你可以在 Github 上面了解该框架的配置和使用方式。这里我们只讲解一下在组件化开发的时候需要注意的地方。注意到 ARouter 是通过注解来进行页面配置的,并且它的注解是在编译的时候进行处理的。所以,我们需要引入 arouter-compiler 来使用它的编译时处理功能。需要注意的地方是,我们只要在公共的模块中加入 arouter-api 就可以使用 ARouter 的 API 了,但是需要在每个模块中引入 arouter-compiler 才能使用编译时注解。也就是说,我们需要在每个模块中都加入 arouter-compiler 依赖。

ARouter 的实现原理使用了 Java 中的注解处理,你可以通过阅读我之前写的一篇文章来了解 《Java 开发者核心技能之 Java 注解及其典型的使用方法》。另外,我们也会在后续的文章中分析 ARouter 的实现原理,欢迎关注我的公众号「Code Brick」。

3. 模块独立

为了能够将各个模块编译成一个独立的 APP,我们需要在 Gradle 里面做一些配置。

首先,我们需要在 gradle.properties 定义一些布尔类型的变量用来判断各个模块是作为一个 library 还是 application 进行编译。这里我的配置如下面的代码所示。也就是,我为每个模块都定义了这么一个布尔类型的变量,当然,你也可以只定义一个变量,然后在各个模块中使用同一个变量来进行判断。
isGuokrModuleApp=false
isLiveModuleApp=false
isLayoutModuleApp=false
isLibraryModuleApp=false
isEyepetizerModuleApp=false


然后,我们来看一下各个模块中的 gradle 该如何配置,这里我们以开眼视频的功能模块作为例子来进行讲解。首先,一个模块作为 library 还是 application 是根据引用的 plugin 来决定的,所以,我们要根据之前定义的布尔变量来决定使用的 plugin:

if (isEyepetizerModuleApp.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}


假如我们要将某个模块作为一个独立的 APP,那么启动类你肯定需要配置。这就意味着你需要两个 AndroidManifest.xml 文件,一个用于 library 状态,一个用于 application 状态。所以,我们可以在 main 目录下面再定义一个 AndroidManifest.xml,然后,我们在该配置文件中不只指定启动类,还使用我们定义的 Application。指定 Application 有时候是必须的,比如你需要在各个模块里面初始化 ARouter 等等。这部分代码就不给出了,可以参考源码,这里我们给出一下在 Gradle 里面指定 AndroidManifest.xml 的方式。

如下所示,我们可以根据之前定义的布尔值来决定使用哪一个配置文件:

sourceSets {
main {
jniLibs.srcDirs = ['libs']
if (isEyepetizerModuleApp.toBoolean()) {
manifest.srcFile "src/main/debug/AndroidManifest.xml"
} else {
manifest.srcFile "src/main/AndroidManifest.xml"
}
}
}


此外,还需要注意的是,如果我们希望在每个模块中都能应用 DataBinding 和 Java 8 的一些特性,那么你需要在每个模块里面都加入下面的配置:

// use data binding
dataBinding {
enabled = true
}
// use java 8 language
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}


对于编译时注解之类的配置,我们也需要在每个模块里面都进行声明。

完成了以上的配置,我们只要根据需要编译的类型,修改之前定义的布尔值,来决定是将该模块编译成 APP 还是作为类库来使用即可。

以上就是组件化在 Android 开发当中的应用。

总结
MVC、MVP 和 MVVM 各有各自的特点,可以根据应用开发的需要选择适合自己的架构模式。组件化的目的就在于保持各个模块之间的独立从而便于分工协作。它们之间的关系就是,你可以在组件化的各个模块中应用前面三种架构模式的一种或者几种。
原文链接:https://mp.weixin.qq.com/s?__biz=MzA3MzgzMzgyNw==&mid=2247483866&idx=1&sn=5b3746c7b82e779882cc5c670a30b217&chksm=9f084dd6a87fc4c0453eeb10f1f8442bdae225fb1f6cad7b2655a20383507d5709d9e3aa749f&token=1361179115&lang=zh_CN#rd

收起阅读 »

时间选择器和选项选择器-Android-PickerView

Android-PickerView介绍这是一款仿iOS的PickerView控件,有时间选择器和选项选择器,新版本的详细特性如下:——TimePickerView 时间选择器,支持年月日时分,年月日,年月,时分等格式。——OptionsPickerView ...
继续阅读 »

Android-PickerView

介绍

这是一款仿iOS的PickerView控件,有时间选择器和选项选择器,新版本的详细特性如下:

——TimePickerView 时间选择器,支持年月日时分,年月日,年月,时分等格式。
——OptionsPickerView 选项选择器,支持一,二,三级选项选择,并且可以设置是否联动 。

  • 支持三级联动
  • 设置是否联动
  • 设置循环模式
  • 支持自定义布局。
  • 支持item的分隔线设置。
  • 支持item间距设置。
  • 时间选择器支持起始和终止日期设定。
  • 支持“年,月,日,时,分,秒”,“省,市,区”等选项的单位(label)显示、隐藏和自定义。
  • 支持自定义文字、颜色、文字大小等属性
  • Item的文字长度过长时,文字会自适应缩放到Item的长度,避免显示不完全的问题
  • 支持Dialog 模式。
  • 支持自定义设置容器。
  • 实时回调。

使用注意事项

  • 注意:当我们进行设置时间的启始位置时,需要特别注意月份的设定
  • 原因:Calendar组件内部的月份,是从0开始的,即0-11代表1-12月份
  • 错误使用案例: startDate.set(2013,1,1);  endDate.set(2020,12,1);
  • 正确使用案例: startDate.set(2013,0,1);  endDate.set(2020,11,1);

如何使用:

Android-PickerView 库使用示例:

1.添加Jcenter仓库 Gradle依赖:

compile 'com.contrarywind:Android-PickerView:4.1.9'


或者

Maven


com.contrarywind
Android-PickerView
4.1.9
pom


2.在项目中添加如下代码:

//时间选择器
TimePickerView pvTime = new TimePickerBuilder(MainActivity.this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date, View v) {
Toast.makeText(MainActivity.this, getTime(date), Toast.LENGTH_SHORT).show();
}
}).build();

//条件选择器
OptionsPickerView pvOptions = new OptionsPickerBuilder(MainActivity.this, new OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3 ,View v) {
//返回的分别是三个级别的选中位置
String tx = options1Items.get(options1).getPickerViewText()
+ options2Items.get(options1).get(option2)
+ options3Items.get(options1).get(option2).get(options3).getPickerViewText();
tvOptions.setText(tx);
}
}).build();
pvOptions.setPicker(options1Items, options2Items, options3Items);
pvOptions.show();


大功告成~

3.如果默认样式不符合你的口味,可以自定义各种属性:

Calendar selectedDate = Calendar.getInstance();
Calendar startDate = Calendar.getInstance();
//startDate.set(2013,1,1);
Calendar endDate = Calendar.getInstance();
//endDate.set(2020,1,1);

//正确设置方式 原因:注意事项有说明
startDate.set(2013,0,1);
endDate.set(2020,11,31);

pvTime = new TimePickerBuilder(this, new OnTimeSelectListener() {
@Override
public void onTimeSelect(Date date,View v) {//选中事件回调
tvTime.setText(getTime(date));
}
})
.setType(new boolean[]{true, true, true, true, true, true})// 默认全部显示
.setCancelText("Cancel")//取消按钮文字
.setSubmitText("Sure")//确认按钮文字
.setContentSize(18)//滚轮文字大小
.setTitleSize(20)//标题文字大小
.setTitleText("Title")//标题文字
.setOutSideCancelable(false)//点击屏幕,点在控件外部范围时,是否取消显示
.isCyclic(true)//是否循环滚动
.setTitleColor(Color.BLACK)//标题文字颜色
.setSubmitColor(Color.BLUE)//确定按钮文字颜色
.setCancelColor(Color.BLUE)//取消按钮文字颜色
.setTitleBgColor(0xFF666666)//标题背景颜色 Night mode
.setBgColor(0xFF333333)//滚轮背景颜色 Night mode
.setDate(selectedDate)// 如果不设置的话,默认是系统时间*/
.setRangDate(startDate,endDate)//起始终止年月日设定
.setLabel("年","月","日","时","分","秒")//默认设置为年月日时分秒
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.isDialog(true)//是否显示为对话框样式
.build();
pvOptions = new OptionsPickerBuilder(this, new OptionsPickerView.OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3 ,View v) {
//返回的分别是三个级别的选中位置
String tx = options1Items.get(options1).getPickerViewText()
+ options2Items.get(options1).get(option2)
+ options3Items.get(options1).get(option2).get(options3).getPickerViewText();
tvOptions.setText(tx);
}
}) .setOptionsSelectChangeListener(new OnOptionsSelectChangeListener() {
@Override
public void onOptionsSelectChanged(int options1, int options2, int options3) {
String str = "options1: " + options1 + "\noptions2: " + options2 + "\noptions3: " + options3;
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
})
.setSubmitText("确定")//确定按钮文字
.setCancelText("取消")//取消按钮文字
.setTitleText("城市选择")//标题
.setSubCalSize(18)//确定和取消文字大小
.setTitleSize(20)//标题文字大小
.setTitleColor(Color.BLACK)//标题文字颜色
.setSubmitColor(Color.BLUE)//确定按钮文字颜色
.setCancelColor(Color.BLUE)//取消按钮文字颜色
.setTitleBgColor(0xFF333333)//标题背景颜色 Night mode
.setBgColor(0xFF000000)//滚轮背景颜色 Night mode
.setContentTextSize(18)//滚轮文字大小
.setLinkage(false)//设置是否联动,默认true
.setLabels("省", "市", "区")//设置选择的三级单位
.isCenterLabel(false) //是否只显示中间选中项的label文字,false则每项item全部都带有label。
.setCyclic(false, false, false)//循环与否
.setSelectOptions(1, 1, 1) //设置默认选中项
.setOutSideCancelable(false)//点击外部dismiss default true
.isDialog(true)//是否显示为对话框样式
.isRestoreItem(true)//切换时是否还原,设置默认选中第一项。
.build();

pvOptions.setPicker(options1Items, options2Items, options3Items);//添加数据源


4.如果需要自定义布局:

// 注意:自定义布局中,id为 optionspicker 或者 timepicker 的布局以及其子控件必须要有,否则会报空指针
// 具体可参考demo 里面的两个自定义布局
pvCustomOptions = new OptionsPickerBuilder(this, new OptionsPickerView.OnOptionsSelectListener() {
@Override
public void onOptionsSelect(int options1, int option2, int options3, View v) {
//返回的分别是三个级别的选中位置
String tx = cardItem.get(options1).getPickerViewText();
btn_CustomOptions.setText(tx);
}
})
.setLayoutRes(R.layout.pickerview_custom_options, new CustomListener() {
@Override
public void customLayout(View v) {
//自定义布局中的控件初始化及事件处理
final TextView tvSubmit = (TextView) v.findViewById(R.id.tv_finish);
final TextView tvAdd = (TextView) v.findViewById(R.id.tv_add);
ImageView ivCancel = (ImageView) v.findViewById(R.id.iv_cancel);
tvSubmit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pvCustomOptions.returnData(tvSubmit);
}
});
ivCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pvCustomOptions.dismiss();
}
});

tvAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getData();
pvCustomOptions.setPicker(cardItem);
}
});

}
})
.build();
pvCustomOptions.setPicker(cardItem);//添加数据


5.对使用还有疑问的话,可参考demo代码

请戳我查看demo代码

6.若只需要WheelView基础控件自行扩展实现逻辑,可直接添加基础控件库,Gradle 依赖:

compile 'com.contrarywind:wheelview:4.1.0'


WheelView 使用代码示例:

xml布局:

            android:id="@+id/wheelview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />


Java 代码:

WheelView wheelView = findViewById(R.id.wheelview);

wheelView.setCyclic(false);

final List mOptionsItems = new ArrayList<>();
mOptionsItems.add("item0");
mOptionsItems.add("item1");
mOptionsItems.add("item2");

wheelView.setAdapter(new ArrayWheelAdapter(mOptionsItems));
wheelView.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(int index) {
Toast.makeText(MainActivity.this, "" + mOptionsItems.get(index), Toast.LENGTH_SHORT).show();
}
});


代码下载:Android-PickerView-master.zip

原文链接:https://github.com/Bigkoo/Android-PickerView

收起阅读 »

今日头条屏幕适配方案终极版正式发布!

前言 我在前面两篇文章中详细介绍了 今日头条适配方案 和 SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定...
继续阅读 »

前言


我在前面两篇文章中详细介绍了 今日头条适配方案SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定的用户基数


但是对于一些才接触这两个方案的朋友,肯定或多或少还是不知道如何选择这两个方案,我虽然在之前的文章中给出了它们各自的优缺点,但是并没有用统一的标准对它们进行更细致的对比,所以也就没办法更形象的体现它们的优劣,那下面我就用统一的标准对它们进行对比,看看它们的对比情况


方案对比


我始终坚定地认为在这两个方案中,并不能以单个标准就能评判出谁一定比谁好,因为它们都有各自的优缺点,都不是完美的,从更客观的角度来看,它们谁都不能成为最好的那个,只有可能明确了它们各自的优缺点,知道在它们的优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,从而选择出一个最适合自己项目的屏幕适配方案


单纯的争论谁是最好的 Android 屏幕适配方案没有任何意义,每个人的需求不一样,站的角度不一样,评判标准也不一样,你能接受的东西他不一定能接受,你觉得不可接受的东西他却觉得可以接受,你有你的理由,他有他的理由,想让一个观点让所有人都能接受太难了!所以我在这里只是列出它们的对比项和对比结果,尽可能的做到客观,最后的选择结果请自行决定,如果还有什么遗漏的对比项,请补充!


































































对比项目 对比对象 A 对比结果 对比对象 B
适配效果(越高越好) 今日头条适配方案 SW 限定符适配方案(在未覆盖的机型上会存在一定的误差)
稳定性(越高越好) 今日头条适配方案 < SW 限定符适配方案
灵活性(越高越好) 今日头条适配方案 > SW 限定符适配方案
扩展性(越高越好) 今日头条适配方案 > SW 限定符适配方案
侵入性(越低越好) 今日头条适配方案 < SW 限定符适配方案
使用成本(越低越好) 今日头条适配方案 < SW 限定符适配方案
维护成本(越低越好) 今日头条适配方案 < SW 限定符适配方案
性能损耗 今日头条适配方案没有性能损耗 = SW 限定符适配方案没有性能损耗
副作用 今日头条适配方案会影响一些三方库和系统控件 SW 限定符适配方案会影响 App 的体积

可以看到 SmallestWidth 限定符适配方案今日头条适配方案 的适配效果其实都是差不多的,我在前面的文章中也通过公式计算过它们的精确度,SmallestWidth 限定符适配方案 运行在未覆盖的机型上虽然也可以适配,但是却会出现一定的误差,所以 今日头条适配方案 的适配精确度确实要比 SmallestWidth 限定符适配方案 略高的,不过只要 SmallestWidth 限定符适配方案 合理的分配资源文件,适配效果的差距应该也不大


SmallestWidth 限定符适配方案 主打的是稳定性,在运行过程中极少会出现安全隐患,适配范围也可控,不会产生其他未知的影响,而 今日头条适配方案 主打的是降低开发成本、提高开发效率,使用上更灵活,也能满足更多的扩展需求,简单一句话概括就是,这两兄弟,一个求稳,一个求快,好了,我就介绍这么多了,自己选择吧!



由来


下面就开始介绍我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize,大家千万不要认为,我推出的屏幕适配框架 AndroidAutoSize 是根据 今日头条屏幕适配方案 优化的,我本人就一定支持 今日头条屏幕适配方案 是最好的 Android 屏幕适配方案这个观点,它确实很优秀,但同样也有很多不足,我最真实的观点在上面就已经表述咯,至于我为什么要根据 今日头条屏幕适配方案 再封装一个屏幕适配框架,无外乎就以下几点原因:



  • SmallestWidth 限定符适配方案 已经有多个优秀的开源解决方案了,它们已经能满足我们日常开发中的所有需求


  • 今日头条 官方技术团队只公布了 今日头条屏幕适配方案文章 以及核心代码,但并没有在 Github 上创建公开的仓库,一个新的方案必定要有一个成长迭代的过程,在此期间,一定需要一个可以把所有使用者聚集起来的公共社区,可以让所有使用该方案的使用者在上面交流,大家一起总结、一起填坑,这样才能让该方案更成熟稳定,这就是开源的力量


  • 今日头条 官方技术团队公布的核心代码并不能满足我的所有需求,已经开源的其他基于 今日头条屏幕适配方案 的开源项目以及解决方案也不能满足我的所有需求,而我有更好的实现想法


  • MVPArms 需要一个适配效果还不错并且切换维护成本也比较低的屏幕适配框架,以帮助使用者用较低的成本、工作量将已经停止维护的 AndroidAutoLayout 快速替换掉



我建议大家都可以去实际体验一下 今日头条屏幕适配方案SmallestWidth 限定符适配方案,感受下它们的异同,我给的建议是,可以在项目中先使用 今日头条屏幕适配方案,感受下它的使用方式以及适配效果,今日头条屏幕适配方案 的侵入性非常低,如果在使用过程中遇到什么不能解决的问题,马上可以切换为其他的屏幕适配方案,在切换的过程中也花费不了多少工作量,试错成本非常低


但如果你在项目中先使用 SmallestWidth 限定符适配方案,之后在使用的过程中再遇到什么不能解决的问题,这时想切换为其他的屏幕适配方案,这工作量可就大了,每个 Layout 文件都含有大量的 dimens 引用,改起来这工作量得有多大,想想都觉得后怕,这就是侵入性太高导致的最致命的问题


与今日头条屏幕适配方案的关系


AndroidAutoSize今日头条屏幕适配方案 的关系,相当于汽车和发动机的关系,今日头条屏幕适配方案 官方公布的代码,只实现了修改系统 density 的相关逻辑,这的确在屏幕适配中起到了最关键的作用,但这还远远还不够


要想让使用者能够更傻瓜式的使用该方案,并且能够应对日常开发中的所有复杂需求,那在架构框架时,还需要考虑 API 的易用性以及合理性、框架的扩展性以及灵活性、功能的全面性、注释和文档的易读性等多个方面的问题


于是我带着我的这些标准在网上搜寻了很久,发现并没有任何一个开源框架或解决方案能够达到我的所有标准,它们大多数还只是停留在将 今日头条屏幕适配方案 封装成工具类来引入项目的阶段,这样在功能的扩展上有限制,并且对用户的使用体验也不好,而我想做的是一个全面性的产品级屏幕适配框架,这离我最初的构想,差距还非常大,于是我只好自己动手,将我的所有思想实现,这才有了 AndroidAutoSize


写完 AndroidAutoSize 框架后,因为对 今日头条屏幕适配方案 有了更加深入的理解,所以才写了 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案,以帮助大家更清晰的理解 今日头条屏幕适配方案


与 AndroidAutoLayout 的关系


AndroidAutoSize 因为名字和 鸿神AndroidAutoLayout 非常相似,并且在填写设计图尺寸的方式上也极为相似,再加上我写的屏幕适配系列的文章也发布在了 鸿神 的公众号上,所以很多人以为 AndroidAutoSize鸿神 写的 AndroidAutoLayout 的升级版,这里我哭笑不得 😂,我只好在这里说一句,大家好,我叫 JessYan,的确可以理解为 AndroidAutoSizeAndroidAutoLayout 的升级版,但是它是我写的,关注一波呗


AndroidAutoSizeAndroidAutoLayout 的原理,却天差地别,比如 AndroidAutoLayout 只能使用 px 作为布局单位,而 AndroidAutoSize 恰好相反,在布局中 dp、sp、pt、in、mm 所有的单位都能支持,唯独不支持 px,但这也意味着 AndroidAutoSizeAndroidAutoLayout 在项目中可以共存,互不影响,所以使用 AndroidAutoLayout 的老项目也可以放心的引入 AndroidAutoSize,慢慢的完成屏幕适配框架的切换


之所以将框架取名为 AndroidAutoSize,第一,是想致敬 AndroidAutoLayoutAndroid 屏幕适配领域的贡献,第二,也想成为在 Android 屏幕适配领域有重要影响力的框架


结构


我在上面就已经说了很多开源框架以及解决方案,只是把 今日头条屏幕适配方案 简单的封装成一个工具类然后引入项目,这时很多人就会说了 今日头条屏幕适配方案 官方公布的全部代码都只有 30 行不到,你不把它封装成工具类,那封装成什么?该怎么封装?下面就来看看 AndroidAutoSize 的整体结构

├── external
│ ├── ExternalAdaptInfo.java
│ ├── ExternalAdaptManager.java
│── internal
│ ├── CancelAdapt.java
│ ├── CustomAdapt.java
│── unit
│ ├── Subunits.java
│ ├── UnitsManager.java
│── utils
│ ├── AutoSizeUtils.java
│ ├── LogUtils.java
│ ├── Preconditions.java
│ ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java

AndroidAutoSize 根据 今日头条屏幕适配方案 官方公布的 30 行不到的代码,经过不断的优化和扩展,发展成了现在拥有 18 个类文件,上千行代码的全面性屏幕适配框架,在迭代的过程中完善和优化了很多功能,相比 今日头条屏幕适配方案 官方公布的原始代码,AndroidAutoSize 更加稳定、更加易用、更加强大,欢迎阅读源码,注释非常详细哦!


功能介绍


AndroidAutoSize 在使用上非常简单,只需要填写设计图尺寸这一步即可接入项目,但需要注意的是,AndroidAutoSize 有两种类型的布局单位可以选择,一个是 主单位 (dp、sp),一个是 副单位 (pt、in、mm),两种单位面向的应用场景都有不同,也都有各自的优缺点



  • 主单位: 使用 dp、sp 为单位进行布局,侵入性最低,会影响其他三方库页面、三方库控件以及系统控件的布局效果,但 AndroidAutoSize 也通过这个特性,使用 ExternalAdaptManager 实现了在不修改三方库源码的情况下适配三方库的功能


  • 副单位: 使用 pt、in、mm 为单位进行布局,侵入性高,对老项目的支持比较好,不会影响其他三方库页面、三方库控件以及系统控件的布局效果,可以彻底的屏蔽修改 density 所造成的所有未知和已知问题,但这样 AndroidAutoSize 也就无法对三方库进行适配



大家可以根据自己的应用场景在 主单位副单位 中选择一个作为布局单位,建议想引入老项目并且注重稳定性的人群使用 副单位,只是想试试本框架,随时可能切换为其他屏幕适配方案的人群使用 主单位


其实 AndroidAutoSize 可以同时支持 主单位副单位,但 AndroidAutoSize 可以同时支持 主单位副单位 的目的,只是为了让使用者可以在 主单位副单位 之间灵活切换,因为切换单位的工作量可能非常巨大,不能立即完成,但领导又要求马上打包上线,这时就可以起到一个很好的过渡作用


主单位


主单位Demodemo


基本使用


AndroidAutoSize 引入项目后,只要在 appAndroidManifest.xml 中填写上设计图尺寸,无需其他过多配置 (如果你没有其他自定义需求的话),AndroidAutoSize 即可自动运行,像下面这样👇

                





在使用主单位时,design_width_in_dpdesign_height_in_dp 的单位必须是 dp,如果设计师给你的设计图,只标注了 px 尺寸 (现在已经有很多 UI 工具可以自动标注 dp 尺寸了),那请自行根据公式 dp = px / (DPI / 160)px 尺寸转换为 dp 尺寸,如果你不知道 DPI 是多少?那请以自己测试机的 DPI 为准,如果连怎么得到设备的 DPI 都不知道?百度吧好伐,如果你实在找不到设备的 DPI 那就直接将 px 尺寸除以 3 或者 2 也是可以的


如果你只是想使用 AndroidAutoSize 的基础功能,AndroidAutoSize 的使用方法在这里就结束了,只需要上面这一步,即可帮助你以最简单的方式接入 AndroidAutoSize,但是作为一个全面性的屏幕适配框架,在保证基础功能的简易性的同时,也必须保证复杂的需求也能在框架内被解决,从而达到一个小闭环,所以下面介绍的内容全是前人踩坑踩出来的一些必备功能,如果你没这个需求,或者觉得麻烦,可以按需查看或者跳过,下面的内容建议和 Demo 配合起来阅读,效果更佳


注意事项



  • 你在 AndroidManifest.xml 中怎么把设计图的 px 尺寸转换为 dp 尺寸,那在布局时,每个控件的大小也需要以同样的方式将设计图上标注的 px 尺寸转换为 dp 尺寸,千万不要在 AndroidManifest.xml 中填写的是 dp 尺寸,却在布局中继续填写设计图上标注的 px 尺寸




  • design_width_in_dpdesign_height_in_dp 虽然都需要填写,但是 AndroidAutoSize 只会将高度和宽度其中的一个作为基准进行适配,一方作为基准,另一方就会变为备用,默认以宽度为基准进行适配,可以通过 AutoSizeConfig#setBaseOnWidth(Boolean) 不停的切换,这意味着最后运行到设备上的布局效果,在高度和宽度中只有一方可以和设计图上一模一样,另外一方会和设计图出现偏差,为什么不像 AndroidAutoLayout 一样,高和宽都以设计图的效果等比例完美呈现呢?这也很简单,你无法保证所有设备的高宽比例都和你设计图上的高宽比例一致,特别是在现在全面屏全面推出的情况下,如果这里不这样做的话,当你的项目运行在与设计图高宽比例不一致的设备上时,布局会出现严重的变形,这个几率非常大,详情请看 这里


自动运行是如何做到的?


很多人有疑惑,为什么使用者只需要在 AndroidManifest.xml 中填写一下 meta-data 标签,其他什么都不做,AndroidAutoSize 就能自动运行,并在 App 启动时自动解析 AndroidManifest.xml 中填写的设计图尺寸,这里很多人不敢相信,问我真的只需要填写下设计图尺寸框架就可以正常运行吗?难道使用了什么 黑科技?


其实这里并没有用到什么 黑科技,原理反而非常简单,只需要声明一个 ContentProvider,在它的 onCreate 方法中启动框架即可,在 App 启动时,系统会在 App 的主进程中自动实例化你声明的这个 ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作,get 到了吗?


这里需要注意的是,如果你的项目拥有多进程,系统只会在主进程中实例化一个你声明的 ContentProvider,并不会在其他非主进程中实例化 ContentProvider,如果在当前进程中 ContentProvider 没有被实例化,那 ContentProvider#onCreate 就不会被调用,你的初始化代码在当前进程中也就不会执行,这时就需要在 Application#onCreate 中调用下 ContentProvider#query 执行一下查询操作,这时 ContentProvider 就会在当前进程中实例化 (每个进程中只会保证有一个实例),所以应用到框架中就是,如果你需要在多个进程中都进行屏幕适配,那就需要在 Application#onCreate 中调用 AutoSize#initCompatMultiProcess 方法


进阶使用


虽然 AndroidAutoSize 不需要其他过多的配置,只需要在 AndroidManifest.xml 中填写下设计图尺寸就能正常运行,但 AndroidAutoSize 还是为大家准备了很多可配置选项,尽最大可能满足大家日常开发中的所有扩展需求


所有的全局配置选项在 Demo 中都有介绍,每个 API 中也都有详细的注释,在这里就不过多介绍了


自定义 Activity


AndroidManifest.xml 中填写的设计图尺寸,是整个项目的全局设计图尺寸,但是如果某些 Activity 页面由于某些原因,设计师单独出图,这个页面的设计图尺寸和在 AndroidManifest.xml 中填写的设计图尺寸不一样该怎么办呢?不要急,AndroidAutoSize 已经为你考虑好了,让这个页面的 Activity 实现 CustomAdapt 接口即可实现你的需求,CustomAdapt 接口的第一个方法可以修改当前页面的设计图尺寸,第二个方法可以切换当前页面的适配基准,下面的注释都解释的很清楚

public class CustomAdaptActivity extends AppCompatActivity implements CustomAdapt {

/**
* 是否按照宽度进行等比例适配 (为了保证在高宽比不同的屏幕上也能正常适配, 所以只能在宽度和高度之中选择一个作为基准进行适配)
*
*
@return {@code true} 为按照宽度进行适配, {@code false} 为按照高度进行适配
*/

@Override
public boolean isBaseOnWidth() {
return false;
}

/**
* 这里使用 iPhone 的设计图, iPhone 的设计图尺寸为 750px * 1334px, 高换算成 dp 为 667 (1334px / 2 = 667dp)
*


* 返回设计图上的设计尺寸, 单位 dp
* {
@link #getSizeInDp} 须配合 {@link #isBaseOnWidth()} 使用, 规则如下:
* 如果 {
@link #isBaseOnWidth()} 返回 {@code true}, {@link #getSizeInDp} 则应该返回设计图的总宽度
* 如果 {
@link #isBaseOnWidth()} 返回 {@code false}, {@link #getSizeInDp} 则应该返回设计图的总高度
* 如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸, {
@link #getSizeInDp} 则返回 {@code 0}
*
*
@return 设计图上的设计尺寸, 单位 dp
*/

@Override
public float getSizeInDp() {
return 667;
}
}


如果某个 Activity 想放弃适配,让这个 Activity 实现 CancelAdapt 接口即可,比如修改 density 影响到了老项目中的某些 Activity 页面的布局效果,这时就可以让这个 Activity 实现 CancelAdapt 接口

public class CancelAdaptActivity extends AppCompatActivity implements CancelAdapt {

}

自定义 Fragment


Fragment 的自定义方式和 Activity 是一样的,只不过在使用前需要先在 App 初始化时开启对 Fragment 的支持

AutoSizeConfig.getInstance().setCustomFragment(true);

实现 CustomAdapt

public class CustomAdaptFragment extends Fragment implements CustomAdapt {

@Override
public boolean isBaseOnWidth() {
return false;
}

@Override
public float getSizeInDp() {
return 667;
}
}

实现 CancelAdapt

public class CancelAdaptFragment extends Fragment implements CancelAdapt {

}

适配三方库页面


在使用主单位时可以使用 ExternalAdaptManager 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (Activity、Fragment)


由于 AndroidAutoSize 要求需要自定义适配参数或取消适配的页面必须实现 CustomAdaptCancelAdapt,这时问题就来了,三方库是通过远程依赖的,我们无法修改它的源码,这时我们怎么让三方库的页面也能实现自定义适配参数或取消适配呢?别急,这个需求 AndroidAutoSize 也已经为你考虑好了,当然不会让你将三方库下载到本地然后改源码!



  • 通过 ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo) 将需要自定义的类和自定义适配参数添加进方法即可替代实现 CustomAdapt 的方式,这里 展示了使用方式,以及详细的注释


  • 通过 ExternalAdaptManager#addCancelAdaptOfActivity(Class) 将需要取消适配的类添加进方法即可替代实现 CancelAdapt 的方式,这里 也展示了使用方式,以及详细的注释



需要注意的是 ExternalAdaptManager 的方法虽然可以添加任何类,但是只能支持 Activity、Fragment,并且 ExternalAdaptManager 是支持链式调用的,以便于持续添加多个页面


当然 ExternalAdaptManager 不仅可以对三方库的页面使用,也可以让自己项目中的 Activity、Fragment 不用实现 CustomAdaptCancelAdapt 即可达到自定义适配参数和取消适配的功能


副单位


前面已经介绍了 副单位 的应用场景,这里就直接介绍 副单位 如何使用,副单位Demodemo-subunits


基本使用


首先和 主单位 一样也需要先在 appAndroidManifest.xml 中填写上设计图尺寸,但和 主单位 不一样的是,当在使用 副单位design_width_in_dpdesign_height_in_dp 的单位不需要一定是 dp,可以直接填写设计图的 px 尺寸,在布局文件中每个控件的大小也可以直接填写设计图上标注的 px 尺寸,无需再将 px 转换为 dp,这是 副单位的 特性之一,可以帮助大家提高开发效率








由于 AndroidAutoSize 提供了 pt、in、mm 三种类型的 副单位 供使用者选择,所以在使用 副单位 时,还需要在 APP 初始化时,通过 UnitsManager#setSupportSubunits(Subunits) 方法选择一个你喜欢的副单位,然后在布局文件中使用这个副单位进行布局,三种类型的副单位,其实效果都是一样,大家按喜欢的名字选择即可


由于使用副单位是为了彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,所以在使用副单位时建议调用 UnitsManager#setSupportDP(false)UnitsManager#setSupportSP(false),关闭 AndroidAutoSizedpsp 的支持,AndroidAutoSize 为什么不在使用 副单位 时默认关闭对 dpsp 的支持?因为允许同时支持 主单位副单位 可以帮助使用者在 主单位副单位 之间切换时更好的过渡,这点在前面就已经提到过


UnitsManager 的详细使用方法,在 demo-subunits 中都有展示,注释也十分详细


自定义 ActivityFragment


在使用 副单位 时自定义 ActivityFragment 的方式是和 主单位 是一样的,这里就不再过多介绍了


适配三方库页面


如果你的项目在使用 副单位 并且关闭了对 主单位 (dp、sp) 的支持,这时 ExternalAdaptManager 对三方库的页面是不起作用的,只对自己项目中的页面起作用,除非三方库的页面也使用了副单位 (pt、in、mm) 进行布局


其实 副单位 之所以能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,就是因为三方库页面、三方库控件以及系统控件基本上使用的都是 dp、sp 进行布局,所以只要 AndroidAutoSize 关闭了对 dp、sp 的支持,转而使用 副单位 进行布局,就能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响


但这也同样意味着使用 副单位 就不能适配三方库的页面了,ExternalAdaptManager 也就对三方库的页面不起作用了


布局实时预览


在开发阶段布局时的实时预览是一个很重要的环节,很多情况下 Android Studio 提供的默认预览设备并不能完全展示我们的设计图,所以我们就需要自己创建模拟设备,dp、pt、in、mm 这四种单位的模拟设备创建方法请看 这里


总结


AndroidAutoSize 在经历了 240+ commit60+ issues6 个版本 的洗礼后,逐渐的稳定了下来,已经在上个星期发布了首个正式版,在这里要感谢将 AndroidAutoSize 接入到自己项目中的上千个使用者,感谢他们的信赖,AndroidAutoSize 创建的初衷就是为了让所有使用 今日头条屏幕适配方案 的使用者能有一个可以一起交流、沟通的聚集地,所以后面也会持续的收集并解决 今日头条屏幕适配方案的常见问题,让 今日头条屏幕适配方案 变得更加成熟、稳定


至此本系列的第三篇文章也就完结了,这也预示着这个系列连载的终结,这篇文章建议结合系列的第一篇文章 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案 一起看,这样可以对 今日头条屏幕适配方案 有一个更深入的理解,如果你能将整个系列的文章都全部认真看完,那你对 Android 屏幕适配领域的相关知识绝对会有一个飞速的提升!


当你的项目需要切换某个框架时,你会怎么去考察、分析、对比现有的开源方案,并有足够的理由去选择或优化一个最适合自己项目的方案呢?其实整个系列文章可以看作是我怎么去选择同类型开源方案的过程,你以后当遇到同样的选择也可以参照我的思维方式去处理,当然如果以后面试官问到你屏幕适配相关的问题,你能将我如何选择、分析、对比已有方案的过程以及文章中的核心知识点告诉给面试官,那肯定比你直接说一句我使用的是某某开源库有价值得多



作者:JessYan
链接:https://www.jianshu.com/p/4aa23d69d481
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

骚年你的屏幕适配方式该升级了!-SmallestWidth 限定符适配方案

前言 ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案 了解我的朋友一定知道,MVPArms 一直使用的是 鸿神 的 AndroidA...
继续阅读 »

前言


ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案


了解我的朋友一定知道,MVPArms 一直使用的是 鸿神AndroidAutoLayout 屏幕适配方案,得益于 AndroidAutoLayout 的便捷,所以我对屏幕适配领域研究的不是很多,AndroidAutoLayout 停止维护后,我也一直在找寻着替代方案,直到 今日头条屏幕适配方案 刷屏,后来又无意间看到了 smallestWidth 限定符屏幕适配方案,这才慢慢的将研究方向转向了屏幕适配领域


最近一个月才开始慢慢恶补 Android 屏幕适配的相关知识,对这两个方案也进行了更深入的研究,可以说从一个小白慢慢成长而来,所以我明白小白的痛,因此在上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 中,把 今日头条屏幕适配方案 讲得非常的细,尽量把每一个知识点都描述清晰,深怕小白漏掉每一个细节,这篇文章我也会延续上一篇文章的优良传统,将 smallestWidth 限定符屏幕适配方案 的每一个知识点都描述清晰


顺便说一句,感谢大家对 AndroidAutoSize 的支持,我只是在上一篇文章中提了一嘴我刚发布的屏幕适配框架 AndroidAutoSize,还没给出详细的介绍和原理剖析 (原计划在本系列的第三篇文章中发布),AndroidAutoSize 就被大家推上了 Github Trending,一个多星期就拿了 2k+ stars,随着关注度的增加,我在这段时间里也累坏了,issues 就没断过,不到半个月就提交了 200 多次 commit,但累并快乐着,在这里要再次感谢大家对 AndroidAutoSize 的认可




大家要注意了!这些观点其实针对的是所有以百分比缩放布局的库,而不只是今日头条屏幕适配方案,所以这些观点也同样适用于 smallestWidth 限定符屏幕适配方案,这点有很多人存在误解,所以一定要注意!





Android系统碎片化机型以及屏幕尺寸碎片化屏幕分辨率碎片化 有多严重大家可以通过 友盟指数 了解一下,有些时候在某些事情的决断标准上,并不能按照事情的对错来决断,大多数情况还是要分析成本,收益等多种因素,通过利弊来决断,每个人的利弊标准又都不一样,所以每个人的观点也都会有差别,但也都应该得到尊重,所以我只是说说自己的观点,也不否认任何人的观点


方案是死的人是活的,在某些大屏手机或平板电脑上,您也可以采用其他适配方案和百分比库结合使用,比如针对某个屏幕区间的设备单独出一套设计图以显示比小屏幕手机更多更精细的内容,来达到与百分比库互补的效果,没有一个方案可以说自己是完美的,但我们能清晰的认识到不同方案的优缺点,将它们的优点相结合,才能应付更复杂的开发需求,产出最好的产品


友情提示: 下面要介绍的 smallestWidth 限定符屏幕适配方案,原理也同样是按照百分比缩放布局,理论上也会存在上面所说的 大屏手机和小屏手机显示的内容相同 的问题,选择与否请仔细斟酌


简介 smallestWidth 限定符适配方案


这个方案的的使用方式和我们平时在布局中引用 dimens 无异,核心点在于生成 dimens.xml 文件,但是已经有大神帮我们做了这 一步


├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440

如果有人还记得上面这种 宽高限定符屏幕适配方案 的话,就可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案 长这样👇


├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp

原理


其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-swdp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-swdp 文件夹,而对应的 values-swdp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备


如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-swdp 文件夹,则会去寻找与之 最小宽度 (smallestWidth) 相近的 values-swdp 文件夹,系统只会寻找小于或等于当前设备 最小宽度 (smallestWidth)values-swdp,这就是优于 宽高限定符屏幕适配方案 的容错率,并且也可以少生成很多 values-swdp 文件夹,减轻 App 的体积


什么是 smallestWidth


smallestWidth 翻译为中文的意思就是 最小宽度,那这个 最小宽度 是什么意思呢?


系统会根据当前设备屏幕的 最小宽度 来匹配 values-swdp,为什么不是根据 宽度 来匹配,而要加上 最小 这两个字呢?


这就要说到,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度,这个 最小宽度 是根据屏幕来定的,是固定不变的,意思是不管您怎么旋转屏幕,只要这个屏幕的高度大于宽度,那系统就只会认定宽度的值为 最小宽度,反之如果屏幕的宽度大于高度,那系统就会认定屏幕的高度的值为 最小宽度


如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?可以再根据 values-wdp (去掉 sw 中的 s) 生成一套资源文件


如果想区分屏幕的方向来做适配该怎么办呢?那就只有再根据 屏幕方向限定符 生成一套资源文件咯,后缀加上 -land-port 即可,像这样,values-sw400dp-land (最小宽度 400 dp 横向)values-sw400dp-port (最小宽度 400 dp 纵向)


smallestWidth 的值是怎么算的


要先算出当前设备的 smallestWidth 值我们才能知道当前设备该匹配哪个 values-swdp 文件夹


ok,还是按照上一篇文章的叙述方式,现在来举栗说明,帮助大家更好理解


我们假设设备的屏幕信息是 1920 * 1080480 dpi


根据上面的规则我们要在屏幕的高度和宽度中选择值最小的一方作为最小宽度,1080 < 1920,明显 1080 px 就是我们要找的 最小宽度 的值,但 最小宽度 的单位是 dp,所以我们要把 px 转换为 dp


帮助大家再巩固下基础,下面的公式一定不能再忘了!


px / density = dpDPI / 160 = density,所以最终的公式是 px / (DPI / 160) = dp


所以我们得到的 最小宽度 的值是 360 dp (1080 / (480 / 160) = 360)


现在我们已经算出了当前设备的最小宽度是 360 dp,我们晓得系统会根据这个 最小宽度 帮助我们匹配到 values-sw360dp 文件夹下的 dimens.xml 文件,如果项目中没有 values-sw360dp 这个文件夹,系统才会去匹配相近的 values-swdp 文件夹


dimens.xml 文件是整个方案的核心所在,所以接下来我们再来看看 values-sw360dp 文件夹中的这个 dimens.xml 是根据什么原理生成的


dimens.xml 生成原理


因为我们在项目布局中引用的 dimens 的实际值,来源于根据当前设备屏幕的 最小宽度 所匹配的 values-swdp 文件夹中的 dimens.xml,所以搞清楚 dimens.xml 的生成原理,有助于我们理解 smallestWidth 限定符屏幕适配方案


说到 dimens.xml 的生成,就要涉及到两个因数,第一个因素是 最小宽度基准值,第二个因素就是您的项目需要适配哪些 最小宽度,通俗理解就是需要生成多少个 values-swdp 文件夹


第一个因素


最小宽度基准值 是什么意思呢?简单理解就是您需要把设备的屏幕宽度分为多少份,假设我们现在把项目的 最小宽度基准值 定为 360,那这个方案就会理解为您想把所有设备的屏幕宽度都分为 360 份,方案会帮您在 dimens.xml 文件中生成 1360dimens 引用,比如 values-sw360dp 中的 dimens.xml 是长这样的

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<dimen name="dp_8">8dp</dimen>
<dimen name="dp_9">9dp</dimen>
<dimen name="dp_10">10dp</dimen>
...
<dimen name="dp_356">356dp</dimen>
<dimen name="dp_357">357dp</dimen>
<dimen name="dp_358">358dp</dimen>
<dimen name="dp_359">359dp</dimen>
<dimen name="dp_360">360dp</dimen>
</resources>



values-sw360dp 指的是当前设备屏幕的 最小宽度360dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 360dp),把屏幕宽度分为 360 份,刚好每份等于 1dp,所以每个引用都递增 1dp,值最大的 dimens 引用 dp_360 值也是 360dp,刚好覆盖屏幕宽度


下面再来看看将 最小宽度基准值 定为 360 时,values-sw400dp 中的 dimens.xml 长什么样

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.1111dp</dimen>
<dimen name="dp_2">2.2222dp</dimen>
<dimen name="dp_3">3.3333dp</dimen>
<dimen name="dp_4">4.4444dp</dimen>
<dimen name="dp_5">5.5556dp</dimen>
<dimen name="dp_6">6.6667dp</dimen>
<dimen name="dp_7">7.7778dp</dimen>
<dimen name="dp_8">8.8889dp</dimen>
<dimen name="dp_9">10.0000dp</dimen>
<dimen name="dp_10">11.1111dp</dimen>
...
<dimen name="dp_355">394.4444dp</dimen>
<dimen name="dp_356">395.5556dp</dimen>
<dimen name="dp_357">396.6667dp</dimen>
<dimen name="dp_358">397.7778dp</dimen>
<dimen name="dp_359">398.8889dp</dimen>
<dimen name="dp_360">400.0000dp</dimen>
</resources>



values-sw400dp 指的是当前设备屏幕的 最小宽度400dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 400dp),把屏幕宽度同样分为 360份,这时每份就等于 1.1111dp 了,每个引用都递增 1.1111dp,值最大的 dimens 引用 dp_360 同样刚好覆盖屏幕宽度,为 400dp


通过两个 dimens.xml 文件的比较,dimens.xml 的生成原理一目了然,方案会先确定 最小宽度基准值,然后将每个 values-swdp 中的 dimens.xml 文件都分配与 最小宽度基准值 相同的份数,再根据公式 屏幕最小宽度 / 份数 (最小宽度基准值) 求出每份占多少 dp,保证不管在哪个 values-swdp 中,份数 (最小宽度基准值) * 每份占的 dp 值 的结果都是刚好覆盖屏幕宽度,所以在 份数 不变的情况下,只需要根据屏幕的宽度在不同的设备上动态调整 每份占的 dp 值,就能完成适配


这样就能保证不管将项目运行到哪个设备上,只要当前设备能匹配到对应的 values-swdp 文件夹,那布局中的 dimens 引用就能根据当前屏幕的情况进行缩放,保证能完美适配,如果没有匹配到对应的 values-swdp 文件夹,也没关系,它会去寻找与之相近的 values-swdp 文件夹,虽然在这种情况下,布局中的 dimens 引用的值可能有些许误差,但是也能保证最大程度的完成适配


说到这里,那大家就应该就会明白我为什么会说 smallestWidth 限定符屏幕适配方案 的原理也同样是按百分比进行布局,如果在布局中,一个 View 的宽度引用 dp_100,那不管运行到哪个设备上,这个 View 的宽度都是当前设备屏幕总宽度的 360分之100,前提是项目提供有当前设备屏幕对应的 values-swdp,如果没有对应的 values-swdp,就会去寻找相近的 values-swdp,这时就会存在误差了,至于误差是大是小,这就要看您的第二个因数怎么分配了


其实 smallestWidth 限定符屏幕适配方案 的原理和 今日头条屏幕适配方案 挺像的,今日头条屏幕适配方案 是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),而 smallestWidth 限定符屏幕适配方案 同样是根据屏幕的宽度动态调整每个设备 每份占的 dp 值


第二个因素


第二个因数是需要适配哪些 最小宽度?比如您想适配的 最小宽度320dp360dp400dp411dp480dp,那方案就会为您的项目生成 values-sw320dpvalues-sw360dpvalues-sw400dpvalues-sw411dpvalues-sw480dp 这几个资源文件夹,像这样👇


├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp

方案会为您需要适配的 最小宽度,在项目中生成一系列对应的 values-swdp,在前面也说了,如果某个设备没有为它提供对应的 values-swdp,那它就会去寻找相近的 values-swdp,但如果这个相近的 values-swdp 与期望的 values-swdp 差距太大,那适配效果也就会大打折扣


那是不是 values-swdp 文件夹生成的越多,覆盖越多市面上的设备,就越好呢?


也不是,因为每个 values-swdp 文件夹其实都会占用一定的 App 体积,values-swdp 文件夹越多,App 的体积也就会越大


所以一定要合理分配 values-swdp,以越少的 values-swdp 文件夹,覆盖越多的机型


验证方案可行性


原理讲完了,我们还是按照老规矩,来验证一下这个方案是否可行?


假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133)


在使用 smallestWidth 限定符屏幕适配方案 时,需要提供 最小宽度基准值 和需要适配哪些 最小宽度,我们就把 最小宽度基准值 设置为 375 (和 设计图 一致),这时方案就会为我们需要适配的 最小宽度 生成对应的 values-swdp 文件夹,文件夹中的 dimens.xml 文件是由从 1375 组成的 dimens 引用,把所有设备的屏幕宽度都分为 375 份,所以在布局文件中我们应该把这个 View 的高宽都引用 dp_50


下面就来验证下在使用 smallestWidth 限定符屏幕适配方案 的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致


验证设备 1


设备 1 的屏幕总宽度为 1080 px,屏幕总高度为 1920 pxDPI480


设备 1 的屏幕高度大于屏幕宽度,所以 设备 1最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 1最小宽度 的值为 360 dp (1080 / (480 / 160) = 360)


根据 设备 1最小宽度 应该匹配的是 values-sw360dp 这个文件夹,假设 values-sw360dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值375 生成的,360 / 375 = 0.96,所以每份占的 dp 值为 0.96dimens.xml 里面的内容是长下面这样的

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">0.96dp</dimen>
<dimen name="dp_2">1.92dp</dimen>
<dimen name="dp_3">2.88dp</dimen>
<dimen name="dp_4">3.84dp</dimen>
<dimen name="dp_5">4.8dp</dimen>
...
<dimen name="dp_50">48dp</dimen>
...
<dimen name="dp_371">356.16dp</dimen>
<dimen name="dp_372">357.12dp</dimen>
<dimen name="dp_373">358.08dp</dimen>
<dimen name="dp_374">359.04dp</dimen>
<dimen name="dp_375">360dp</dimen>
</resources>



可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw360dp 中定格在了 48 dp,所以这个 View设备 1 上的高宽都为 48 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 144 px (48 * (480 / 160) = 144)


144 / 1080 = 0.133View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


某些设备的高宽是和 设备 1 相同的,但是 DPI 可能不同,而由于 smallestWidth 限定符屏幕适配方案 并没有像 今日头条屏幕适配方案 一样去自行修改 density,所以系统就会使用默认的公式 DPI / 160 求出 densitydensity 又会影响到 dppx 的换算,因此 DPI 的变化,是有可能会影响到 smallestWidth 限定符屏幕适配方案


所以我们再来试试在这种特殊情况下 smallestWidth 限定符屏幕适配方案 是否也能完成适配


验证设备 2


设备 2 的屏幕总宽度为 1080 px,屏幕总高度为 1920 pxDPI420


设备 2 的屏幕高度大于屏幕宽度,所以 设备 2最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 2最小宽度 的值为 411.429 dp (1080 / (420 / 160) = 411.429)


根据 设备 2最小宽度 应该匹配的是 values-sw411dp 这个文件夹,假设 values-sw411dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值375 生成的,411 / 375 = 1.096,所以每份占的 dp 值为 1.096dimens.xml 里面的内容是长下面这样的👇

<?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.096dp</dimen>
<dimen name="dp_2">2.192dp</dimen>
<dimen name="dp_3">3.288dp</dimen>
<dimen name="dp_4">4.384dp</dimen>
<dimen name="dp_5">5.48dp</dimen>
...
<dimen name="dp_50">54.8dp</dimen>
...
<dimen name="dp_371">406.616dp</dimen>
<dimen name="dp_372">407.712dp</dimen>
<dimen name="dp_373">408.808dp</dimen>
<dimen name="dp_374">409.904dp</dimen>
<dimen name="dp_375">411dp</dimen>
</resources>



可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw411dp 中定格在了 54.8dp,所以这个 View设备 2 上的高宽都为 54.8 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 143.85 px (54.8 * (420 / 160) = 143.85)


143.85 / 1080 = 0.133View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


虽然 View设备 2 上的高宽是 143.85 px,比 设备 1144 px 少了 0.15 px,但是误差非常小,整体的比例并没有发生太大的变化,是完全可以接受的


这个误差是怎么引起的呢,因为 设备 2最小宽度 的实际值是 411.429 dp,但是匹配的 values-sw411dp 舍去了小数点后面的位数 (切记!系统会去寻找小于或等于 411.429 dp 的 values-swdp,所以 values-sw412dp 这个文件夹,设备 2 是匹配不了的),所以才存在了一定的误差,因此上面介绍的第二个因数是非常重要的,这直接决定误差是大还是小


可以看到即使在高宽一样但 DPI 不一样的设备上,smallestWidth 限定符屏幕适配方案 也能完成等比例适配,证明这个方案是可行的,如果大家还心存疑虑,也可以再试试其他分辨率的设备,其实到最后得出的比例都是在 0.133 左右,唯一的变数就是第二个因数,如果您生成的 values-swdp 与设备实际的 最小宽度 差别不大,那误差也就在能接受的范围内,如果差别很大,那就直接 GG


优点



  1. 非常稳定,极低概率出现意外


  2. 不会有任何性能的损耗


  3. 适配范围可自由控制,不会影响其他三方库


  4. 在插件的配合下,学习成本低



缺点



  1. 在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦


  2. 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂


  3. 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择


  4. 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积


  5. 不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积


  6. 不能以高度为基准进行适配,考虑到这个方案的名字本身就叫 最小宽度限定符适配方案,所以在使用这个方案之前就应该要知道这个方案只能以宽度为基准进行适配,为什么现在的屏幕适配方案只能以高度或宽度其中的一个作为基准进行适配,请看 这里



使用中的问题


这时有人就会问了,设计师给的设计图只标注了 px,使用这个方案时,那不是还要先将 px 换算成 dp


其实也可以不用换算的,那这是什么骚操作呢?


很简单,你把设计图的 px 总宽度设置成 最小宽度基准值 就可以了,还是以前面验证可行性的例子


我们在前面验证可行性时把 最小宽度基准值 设置成了 375,为什么是 375 呢?因为设计图的总宽度为 375 dp,如果换算成 px,总宽度就是 750 px,我们这时把 最小宽度基准值 设置成 750,然后看看 values-sw360dp 中的 dimens.xml 长什么样👇<?xml version="1.0" encoding="UTF-8"?>

<resources>
<dimen name="px_1">0.48dp</dimen>
<dimen name="px_2">0.96dp</dimen>
<dimen name="px_3">1.44dp</dimen>
<dimen name="px_4">1.92dp</dimen>
<dimen name="px_5">2.4dp</dimen>
...
<dimen name="px_50">24dp</dimen>
...
<dimen name="px_100">48dp</dimen>
...
<dimen name="px_746">358.08dp</dimen>
<dimen name="px_747">358.56dp</dimen>
<dimen name="px_748">359.04dp</dimen>
<dimen name="px_749">359.52dp</dimen>
<dimen name="px_750">360dp</dimen>
</resources>



360 dp 被分成了 750 份,相比之前的 375 份,现在 每份占的 dp 值 正好减少了一半,还记得在验证可行性的例子中那个 View 的尺寸是多少吗?50dp * 50dp,如果设计图只标注 px,那这个 View 在设计图上的的尺寸应该是 100px * 100px,那我们直接根据设计图上标注的 px,想都不用想直接在布局中引用 px_100 就可以了,因为在 375 份时的 dp_50 刚好等于 750 份时的 px_100 (值都是 48 dp),所以这时的适配效果和之前验证可行性时的适配效果没有任何区别


看懂了吗?直接将 最小宽度基准值 和布局中的引用都以 px 作为单位就可以直接填写设计图上标注的 px


总结


关于文中所列出的优缺点,列出的缺点数量确实比列出的优点数量多,但 缺点 3缺点 4缺点 5 其实都可以归纳于 占用 App 体积 这一个缺点,因为他们都可以通过增加资源文件来解决问题,而 缺点 6 则是这个方案的特色,只能以宽度为基准进行适配,这个从这个方案的名字就能看出


请大家千万不要曲解文章的意思,不要只是单纯的对比优缺点的数量,缺点的数量大于优点的数量就一定是这个方案不行?没有一个方案是完美的,每个人的需求也都不一样,作为一篇科普类文章我只可能把这个方案描述得尽可能的全面


这个方案能给你带来什么,不能给你带来什么,我必须客观的描述清楚,这样才有助你做出决定,你应该注重的是在这些优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,而不只是单纯的去看数量,这样毫无意义,有些人就是觉得稳定性最重要,其他的都可以做出妥协,那其他缺点对于他来说都是无所谓的


好了,这个系列的第二篇文章讲完了,这篇文章也是按照上篇文章的优良传统,写的非常详细,哪怕是新手我相信也应该能看懂,为什么这么多人都不知道自己该选择什么样的方案,就是因为自己都没搞懂这些方案的原理,懂了原理过后才知道这些方案是否是自己想要的


接下来的第三篇文章会详细讲解两个方案的深入对比以及该如何选择,并剖析我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize 的原理,敬请期待


如果大家想使用 smallestWidth 限定符屏幕适配方案,可以参考 这篇文章,里面提供有自动生成资源文件的插件和 Demo,由于我并没有在项目中使用 smallestWidth 限定符屏幕适配方案,所以如果在文章中有遗漏的知识点请谅解以及补充,感谢!



作者:JessYan
链接:https://www.jianshu.com/p/2aded8bb6ede
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

骚年你的屏幕适配方式该升级了!-今日头条适配方案

前言 这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案 上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方...
继续阅读 »

前言


这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案


上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方案,并开源了相关源码


上个星期 拉丁吴 老师在 鸿神 的公众号上发布了一篇 文章,详细描述了市面上主流的几种屏幕适配方案,并发布了他的 smallestWidth 限定符适配方案和相关源码 (其实早就发布了),文章写的很好,建议大家去看看


其实大家最关注的不是市面上有多少种屏幕适配方案,而是自己的项目该选择哪种屏幕适配方案,可以看出两位老师最终选择的屏幕适配方案都是不同的


我下面就来分析分析,我作为一个才接触这两个屏幕适配方案的吃瓜群众,我是怎么来验证这两种屏幕适配方案是否可行,以及怎样根据它们的优缺点来选择一个最适合自己项目的屏幕适配方案


浅谈适配方案


拉丁吴 老师的文章中谈到了两个比较经典的屏幕适配方案,在我印象中十分深刻,我想大多数兄弟都用过,在我的开发生涯里也是有很长一段时间都在用这两种屏幕适配方案


第一种就是宽高限定符适配,什么是宽高限定符适配呢

├── src/main

│   ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440

就是这种,在资源文件下生成不同分辨率的资源文件,然后在布局文件中引用对应的 dimens,大家一定还有印象


第二种就是 鸿神AndroidAutoLayout


这两种方案都已经逐渐退出了历史的舞台,为什么想必大家都知道,不知道的建议看看 拉丁吴 老师的文章,所以这两种方案我在文章中就不在阐述了,主要讲讲现在最主流的两种屏幕适配方案,今日头条适配方案smallestWidth 限定符适配方案


建议大家不清楚这两个方案的先看看这两篇文章,才清楚我在讲什么,后面我要讲解它们的原理,以及验证这两种方案是否真的可行,最后对他们进行深入对比,对于他们的一些缺点给予对应的解决方案,绝对干货


今日头条屏幕适配方案


原理


上面已经告知,不了解这两个方案的先看看上面的两篇文章,所以这里我就假设大家已经看了上面的文章或者之前就了解过这两个方案,所以在本文中我就不再阐述 DPIDensity 以及一些比较基础的知识点,上面的文章已经阐述的够清楚了


今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density


当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density


density 的意思就是 1 dp 占当前设备多少像素


为什么要算出 density,这和屏幕适配有什么关系呢?


public static float applyDimension(int unit, float value,
DisplayMetrics metrics
)

{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}

大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 px


所以我们常用的 pxdp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步


要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,同时进行适配呢


这就引出了一个现在比较棘手的问题,大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致


这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题


明白这个后,我再来说说 densitydensity 在每个设备上都是固定的,DPI / 160 = density屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度



  • 设备 1,屏幕宽度为 1080px480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360dp


  • 设备 2,屏幕宽度为 1440560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411dp



可以看到屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的


这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8% (100 / 360 = 0.278)


但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3% (100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差


这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的


这时我们该怎么做呢?改变每个 Viewdp 值?不现实,在每个设备上都要通过代码动态计算 Viewdp 值,工作量太大


如果每个 Viewdp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp


屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度


在这个公式中我们要保证 屏幕的总 dp 宽度设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了


当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density


这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配


验证方案可行性


上面已经把原理分析的很清楚了,很多文章只是一笔带过这个公式,公式虽然很简单但我们还是想晓得这是怎么来的,所以我就反向推理了一遍,如果还是看不懂,那我只能说我尽力了,原理讲完了,那我们再来现场验证一下这个方案是否可行?


假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致


验证设备 1


屏幕总宽度为 1080 px,根据今日头条的的公式求出 density1080 / 375 = 2.88 (density)


这个 50dp * 50dpView,系统最后会将高宽都换算成 px50dp * 2.88 = 144 px (根据公式 dp * density = px)


144 / 1080 = 0.133View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


某些设备总宽度为 1080 px,但是 DPI 可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 DPI 求出 density,是根据自己的公式求出的 density,所以这对今日头条的方案没有影响


上面只能确定在所有屏幕总宽度为 1080 px 的设备上能完成等比例适配,那我们再来试试其他分辨率的设备


验证设备 2


屏幕总宽度为 1440 px,根据今日头条的的公式求出 density1440 / 375 = 3.84 (density)


这个 50dp * 50dpView,系统最后会将高宽都换算成 px50dp * 3.84 = 192 px (根据公式 dp * density = px)


192 / 1440 = 0.133View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放


两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 0.133


优点



  1. 使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案


  2. 侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0


  3. 可适配三方库的控件和系统的控件(不止是 ActivityFragmentDialogToast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益


  4. 不会有任何性能的损耗



缺点


暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的


只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的


这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样


当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重


举个栗子


假设一个三方库的 View,作者在设计时,把它设计为 100dp * 100dp,设计图的最大宽度为 1000dp,这个 View 在设计图中的比例是 100 / 1000 = 0.1,意思是这个 View 的宽度在设计图中占整个宽度的 10%,如果我们要完成等比例适配,那这个三方库 View 在所有的设备上与屏幕的总宽度的比例,都必须保持在 10%


这时在一个使用今日头条屏幕适配方案的项目上,设置的设计图最大宽度如果是 1000dp,那这个三方库 View,与项目自身都可以完美的适配,但当我们项目自身的设计图最大宽度不是 1000dp,是 500dp 时,100 / 500 = 0.2,可以看到,比例发生了较大的变化,从 10% 上升为 20%,明显这个三方库 View 高于作者的预期,比之前更大了


这就是两个设计图尺寸不一致导致的非常严重的问题,当两个设计图尺寸差距越大,那适配的效果也就天差万别了


解决方案


方案 1


调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配


这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变


但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity 为单位修改设计图尺寸,相当于每个 Activity 都可以自定义设计图尺寸,因为有些 Activity 不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity 都有控制权的话,这也是最灵活的


但这也有个问题,当一个 Activity 使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题


方案 2


第二个方案是最简单的,也是按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案


使用中的问题


有些文章中提到了今日头条屏幕适配方案可以将设计图尺寸填写成以 px 为单位的宽度和高度,这样我们在布局文件中,也就能直接填写设计图上标注的 px 值,省掉了将 px 换算为 dp 的时间 (大部分公司的设计图都只标注 px 值),而且照样能完美适配


但是我建议大家千万不要这样做,还是老老实实的以 dp 为单位填写 dp 值,为什么呢?


直接填写 px 虽然刚开始布局的时候很爽,但是这个坑就已经埋上了,会让你后面很爽,有哪些坑?


第一个坑


这样无疑于使项目强耦合于这个方案,当你遇到无法解决的问题想切换为其他屏幕适配方案的时候,layout 文件里曾经填写的 px 值都会作为 dp


比如你的设计图实际宽度为 1080px,你不换算为 360dp (1080 / 3 = 360),却直接将 1080px 作为这个方案的设计图尺寸,那你在 layout 文件中,填写的也都是设计图上标注的 px 值,但是单位却是 dp


一个在设计图上 300px * 300pxView,你可以直接在 layout 文件中填写为 300dp,而由于这个方案可以动态改变 density 的原因还是可以做到等比例适配,非常爽!


但你不要忘了,这样你就强耦合于这个方案了,因为当你不使用这个方案时,density 是不可变的!


举个栗子


使用这个方案时,在屏幕宽度为 1080px 的设备上,将设计图宽度直接填写为 1080,根据今日头条公式


当前设备屏幕总宽度 / 设计图总宽度 = density


这时得出 density 为 1 (1080 / 1080 = 1),所以你在 layout 文件中你填写的 300dp 最后转换为 px 也是 300px (300dp * 1 = 300px 根据公式 dp * density = px)


在这个方案的帮助下非常完美,和设计图一模一样完成了适配


但当你不使用这个方案时,density 的换算公式就变为官方的 DPI / 160 = density

在这个屏幕宽度为 1080px480dpi 的设备上,density 就固定为 3 (480 / 160 = 3)


这时再来看看你之前在 layout 文件中填写的 dp,换算成 px900 px (300dp * 3 = 900px 根据公式 dp * density = px)


原本在在设计图上为 300pxView,这时却达到了惊人的 900px,3倍的差距,恭喜你,你已经强耦合于这个方案了,你要不所有 layout 文件都改一遍,要不继续使用这个方案


第二个坑


第二个坑其实就是刚刚在上面说的今日头条适配方案的缺点,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重


你如果直接填写以 px 为设计图的尺寸,这不用想,肯定和所有的三方库以及系统控件的设计图尺寸都不一样,而且差距都非常之大,至少两三倍的差距,这时你在当前页面弹个 Toast 就可以明显看到,比之前小很多,可以说是天差万别,用其他三方库 View,也是一样的,会小很多


因为你以 px 为单位填写设计图尺寸,人家却用的 dp,差距能不大吗,你如果老老实实用 dp,哪怕三方库的设计图尺寸和你项目自身的设计图尺寸不一样,那也差距不大,小到一定程度,基本都不用调整,可以忽略不计,而且很多三方库的设计图尺寸其实也都是那几个大众尺寸,很大可能和你项目自身的设计图尺寸一样


总结


可以看到我讲的非常详细,可以说比今日头条官方以及任何博客写的都清楚,从原理到优缺点再到解决方案一应俱全,因为篇幅有限,如果我还想把 smallestWidth 限定符适配方案写的这么详细,那估计这篇文章得有一万字了


所以我把这次的屏幕适配文章归位一个系列,一共分为三篇,第一篇详细的讲 今日头条屏幕适配方案,第二篇详细的讲 smallestWidth 限定符适配方案,第三篇详细讲两个方案的深入对比以及如何选择,并发布我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize


今日头条屏幕适配方案 官方公布的核心源码只有 30 行不到,但我这个框架的源码有 1500 行以上,在保留原有特性的情况下增加了不少功能和特性,功能增加了不少,但是使用上却变简单了









只要这一步填写了设计图的高宽以 dp 为单位,你什么都不做,框架就开始适配了


大家可以提前看看我是怎么封装和优化的,我后面的第三篇文章会给出这个框架的原理分析,敬请期待




关于大家的评论以及关注的问题,我在这里统一回复一下:


感谢,大家的关注和回复,我介绍这个今日头条的屏幕适配方案并不是说他有多么完美,只是他确实有效而且能帮我们减少很多开发成本


对于很多人说的 DPI 的存在,不就是为了让大屏能显示更多的内容,如果一个大屏手机和小屏手机,显示的内容都相同,那用户买大屏手机又有什么意义呢,我觉得大家对 DPI 的理解是对的,这个观点我也是认同的,Google 设计 DPI 时可能也是这么想的,但是有一点大家没考虑到,Android 的碎片化太严重了


为什么 Android 诞生这么多年,Android 的百分比库层出不穷,按理说他们都违背了上面说的这个理念,但为什么还有这么多人去研究百分比库通过各种方式去实现百分比布局 (谷歌官方也曾出过百分比库)?为什么?很简单,因为需求啊!为什么需求,因为开发成本低啊!为什么今日头条的这个屏幕适配方案现在能这么火,因为他的开发成本是目前所有屏幕适配方案中最低的啊!


DPI 的意义谁又不懂呢?难道就你懂,今日头条这种大公司的程序员不懂这个道理吗?今日头条这么大的公司难道不想把每个机型每个版本的设备都适配完美,让自己的 App 体验更好,哪怕付出更大的成本,有些时候想象是美好的,但是这个投入的成本,谁又能承担呢,连今日头条这么大的公司,这么雄厚的资本都没选择投入更大的成本,对每个机型进行更精细化的适配,难道市面上的中小型公司又有这个能力投入这么大的成本吗?


鱼和熊掌不可兼得,DPI 的意义在 Google 的设计理念中是完全正确的,但不是所有公司都能承受这个成本,想必今日头条的程序员,也是因为今日头条 App 的用户量足够多,机型分布足够广,也是被屏幕适配这个问题折磨的不要不要的,才想出这么个不这么完美但是却很有效的方案



作者:JessYan
链接:https://www.jianshu.com/p/55e0fca23b4f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android屏幕适配方案-AndroidAutoLayout

AndroidAutoLayoutAndroid屏幕适配方案,直接填写设计图上的像素尺寸即可完成适配。引入Android Studio将autolayout引入dependencies { compile project(':autolayout') ...
继续阅读 »

AndroidAutoLayout

Android屏幕适配方案,直接填写设计图上的像素尺寸即可完成适配。

引入

  • Android Studio

autolayout引入

dependencies {
compile project(':autolayout')
}

也可以直接

dependencies {
compile 'com.zhy:autolayout:1.4.5'
}
  • Eclipse

建议使用As,方便版本更新。实在不行,只有复制粘贴源码了。

用法

第一步:

在你的项目的AndroidManifest中注明你的设计稿的尺寸。

<meta-data android:name="design_width" android:value="768">
meta-data>
<meta-data android:name="design_height" android:value="1280">
meta-data>

第二步:

让你的Activity继承自AutoLayoutActivity.

非常简单的两个步骤,你就可以开始愉快的编写布局了,详细可以参考sample。

其他用法

如果你不希望继承AutoLayoutActivity,可以在编写布局文件时,将

  • LinearLayout -> AutoLinearLayout
  • RelativeLayout -> AutoRelativeLayout
  • FrameLayout -> AutoFrameLayout

这样也可以完成适配。

目前支持属性

  • layout_width
  • layout_height
  • layout_margin(left,top,right,bottom)
  • pading(left,top,right,bottom)
  • textSize
  • maxWidth, minWidth, maxHeight, minHeight

配置

默认使用的高度是设备的可用高度,也就是不包括状态栏和底部的操作栏的,如果你希望拿设备的物理高度进行百分比化:

可以在Application的onCreate方法中进行设置:

public class UseDeviceSizeApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
AutoLayoutConifg.getInstance().useDeviceSize();
}
}

扩展

对于其他继承系统的FrameLayout、LinearLayout、RelativeLayout的控件,比如CardView,如果希望再其内部直接支持"px"百分比化,可以自己扩展,扩展方式为下面的代码,也可参考issue#21

package com.zhy.sample.view;

import android.content.Context;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;

import com.zhy.autolayout.AutoFrameLayout;
import com.zhy.autolayout.utils.AutoLayoutHelper;

/**
* Created by zhy on 15/12/8.
*/

public class AutoCardView extends CardView
{
private final AutoLayoutHelper mHelper = new AutoLayoutHelper(this);

public AutoCardView(Context context)
{
super(context);
}

public AutoCardView(Context context, AttributeSet attrs)
{
super(context, attrs);
}

public AutoCardView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}

@Override
public AutoFrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new AutoFrameLayout.LayoutParams(getContext(), attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (!isInEditMode())
{
mHelper.adjustChildren();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


}

注意事项

ListView、RecyclerView类的Item的适配

sample中包含ListView、RecyclerView例子,具体查看sample

  • 对于ListView

对于ListView这类控件的item,默认根局部写“px”进行适配是无效的,因为外层非AutoXXXLayout,而是ListView。但是,不用怕,一行代码就可以支持了:

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder = null;
if (convertView == null)
{
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
convertView.setTag(holder);
//对于listview,注意添加这一行,即可在item上使用高度
AutoUtils.autoSize(convertView);
} else
{
holder = (ViewHolder) convertView.getTag();
}

return convertView;
}

注意 AutoUtils.autoSize(convertView);这行代码的位置即可。demo中也有相关实例。

  • 对于RecyclerView
public ViewHolder(View itemView)
{
super(itemView);
AutoUtils.autoSize(itemView);
}

//...
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View convertView = LayoutInflater.from(mContext).inflate(R.layout.recyclerview_item, parent, false);
return new ViewHolder(convertView);
}

一定要记得LayoutInflater.from(mContext).inflate使用三个参数的方法!

指定设置的值参考宽度或者高度

由于该库的特点,布局文件中宽高上的1px是不相等的,于是如果需要宽高保持一致的情况,布局中使用属性:

app:layout_auto_basewidth="height",代表height上编写的像素值参考宽度。

app:layout_auto_baseheight="width",代表width上编写的像素值参考高度。

如果需要指定多个值参考宽度即:

app:layout_auto_basewidth="height|padding"

用|隔开,类似gravity的用法,取值为:

  • width,height
  • margin,marginLeft,marginTop,marginRight,marginBottom
  • padding,paddingLeft,paddingTop,paddingRight,paddingBottom
  • textSize.

TextView的高度问题

设计稿一般只会标识一个字体的大小,比如你设置textSize="20px",实际上TextView所占据的高度肯定大于20px,字的上下都会有一定的间隙,所以一定要灵活去写字体的高度,比如对于text上下的margin可以选择尽可能小一点。或者选择别的约束条件去定位(比如上例,选择了marginBottom)

常见问题

###(1)导入后出现org/gradle/api/publication/maven/internal/DefaultMavenFactory

最简单的方式,通过compile 'com.zhy:autolayout:x.x.x'进行依赖使用,

###(2)RadioGroup,Toolbar等控件中的子View无法完成适配

这个其实上文已经提到过了,需要自己扩展。不过这个很多使用者贡献了他们的扩展类可以直接使用, 参考autolayout-widget, 如果没有发现你需要的容器类,那么你就真的需要自行扩展了,当然如果你完成了扩展,可以给我发个PR,或者让我知道,我可以加入到 autolayout-widget中方便他人,ps:需要用到哪个copy就好了,不要直接引用autolayout-widget,因为其引用了大量的库,可能很多 库你是用不到的。

###(3)java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

这个问题是因为默认AutoLayoutActivity会继承自AppCompatActivity,所以默认需要设置 Theme.AppCompat的theme;

如果你使用的依旧是FragmentActivity等,且不考虑使用AppCompatActivity, 你可以选择自己编写一个MyAutoLayoutActivity extends 目前你使用的Activity基类,例如 MyAutoLayoutActivity extends FragmentActivity,然后将该库中AutoLayoutActivity中的逻辑 拷贝进去即可,以后你就继承你的MyAutoLayoutActivity就好了。

ps:还是建议尽快更新SDK版本使用AppCompatActivity.


代码下载:AndroidAutoLayout-master.zip

原文链接:https://github.com/hongyangAndroid/AndroidAutoLayout

收起阅读 »

Android 优雅的为RecyclerView添加HeaderView和FooterView

1、概述RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求。如果你对RecyclerView不陌生的话,你一定遇到过这样的情况,我想给RecyclerView加个headerView或...
继续阅读 »

1、概述

RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求。

如果你对RecyclerView不陌生的话,你一定遇到过这样的情况,我想给RecyclerView加个headerView或者footerView,当你敲出.addHeaderView,你会发现并没有添加头部或者底部View的相关API。

那么本文主要的内容很明显了,完成以下工作:

如何为RecyclerView添加HeaderView(支持多个)
如何为RecyclerView添加FooterView(支持多个)
如何让HeaderView或者FooterView适配各种LayoutManager

那我只能考虑自己换种思路来解决这个问题,并且提供尽可能多的功能了~

2 、思路

(1)原理

对于添加headerView或者footerView的思路

其实HeaderView实际上也是Item的一种,只不过显示在顶部的位置,那么我们完全可以通过为其设置ItemType来完成。

有了思路以后,我们心里就妥了,最起码我们的内心中想想是可以实现的,接下来考虑一些细节。

(2)一些细节

假设我们现在已经完成了RecyclerView的编写,忽然有个需求,需要在列表上加个HeaderView,此时我们该怎么办呢?

打开我们的Adapter,然后按照我们上述的原理,添加特殊的ViewType,然后修改代码完成。

这是比较常规的做法了,但是有个问题是,如果需要添加viewType,那么可能我们的Adapter需要修改的幅度就比较大了,比如getItemType、getItemCount、onBindViewHolder、onCreateViewHolder等,几乎所有的方法都要进行改变。

这样来看,出错率是非常高的。

况且一个项目中可能多个RecyclerView都需要在其列表中添加headerView。

这么来看,直接改Adapter的代码是非常不划算的,最好能够设计一个类,可以无缝的为原有的Adapter添加headerView和footerView。

本文的思路是通过类似装饰者模式,去设计一个类,增强原有Adapter的功能,使其支持addHeaderView和addFooterView。这样我们就可以不去改动我们之前已经完成的代码,灵活的去扩展功能了。

我希望的用法是这样的:

mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
t1.setText("Header 1");
TextView t2 = new TextView(this);
mHeaderAndFooterWrapper.addHeaderView(t2);


在不改变原有的Adapter基础上去增强其功能。

3、初步的实现

(1) 基本代码



首先我们编写一个Adapter的子类,我们叫做HeaderAndFooterWrapper,然后再其内部添加了addHeaderView,addFooterView等一些辅助方法。

这里你可以看到,对于多个HeaderView,讲道理我们首先想到的应该是使用List,而这里我们为什么要使用SparseArrayCompat呢?

public class HeaderAndFooterWrapper extends RecyclerView.Adapter
{
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;

private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat mFootViews = new SparseArrayCompat<>();

private RecyclerView.Adapter mInnerAdapter;

public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
{
mInnerAdapter = adapter;
}

private boolean isHeaderViewPos(int position)
{
return position < getHeadersCount();
}

private boolean isFooterViewPos(int position)
{
return position >= getHeadersCount() + getRealItemCount();
}


public void addHeaderView(View view)
{
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}

public void addFootView(View view)
{
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}

public int getHeadersCount()
{
return mHeaderViews.size();
}

public int getFootersCount()
{
return mFootViews.size();
}
}


SparseArrayCompat有什么特点呢?它类似于Map,只不过在某些情况下比Map的性能要好,并且只能存储key为int的情况。

并且可以看到我们对每个HeaderView,都有一个特定的key与其对应,第一个headerView对应的是BASE_ITEM_TYPE_HEADER,第二个对应的是BASE_ITEM_TYPE_HEADER+1;

为什么要这么做呢?

这两个问题都需要到复写onCreateViewHolder的时候来说明。

(2)复写相关方法

public class HeaderAndFooterWrapper extends RecyclerView.Adapter
{

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
if (mHeaderViews.get(viewType) != null)
{

ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
return holder;

} else if (mFootViews.get(viewType) != null)
{
ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}

@Override
public int getItemViewType(int position)
{
if (isHeaderViewPos(position))
{
return mHeaderViews.keyAt(position);
} else if (isFooterViewPos(position))
{
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}

private int getRealItemCount()
{
return mInnerAdapter.getItemCount();
}


@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (isHeaderViewPos(position))
{
return;
}
if (isFooterViewPos(position))
{
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}

@Override
public int getItemCount()
{
return getHeadersCount() + getFootersCount() + getRealItemCount();
}
}



getItemViewType
由于我们增加了headerView和footerView首先需要复写的就是getItemCount和getItemViewType。

getItemCount很好理解;

对于getItemType,可以看到我们的返回值是mHeaderViews.keyAt(position),这个值其实就是我们addHeaderView时的key,footerView是一样的处理方式,这里可以看出我们为每一个headerView创建了一个itemType。

onCreateViewHolder
可以看到,我们分别判断viewType,如果是headview或者是footerview,我们则为其单独创建ViewHolder,这里的ViewHolder是我之前写的一个通用的库里面的类,文末有链接。当然,你也可以自己写一个ViewHolder的实现类,只需要将对应的headerView作为itemView传入ViewHolder的构造即可。

这个方法中,我们就可以解答之前的问题了:

为什么我要用SparseArrayCompat而不是List?
为什么我要让每个headerView对应一个itemType,而不是固定的一个?
对于headerView假设我们有多个,那么onCreateViewHolder返回的ViewHolder中的itemView应该对应不同的headerView,如果是List,那么不同的headerView应该对应着:list.get(0),list.get(1)等。

但是问题来了,该方法并没有position参数,只有itemType参数,如果itemType还是固定的一个值,那么你是没有办法根据参数得到不同的headerView的。

所以,我利用SparseArrayCompat,将其key作为itemType,value为我们的headerView,在onCreateViewHolder中,直接通过itemType,即可获得我们的headerView,然后构造ViewHolder对象。而且我们的取值是从100000开始的,正常的itemType是从0开始取值的,所以正常情况下,是不可能发生冲突的。

需要说明的是,这里的意思并非是一定不能用List,通过一些特殊的处理,List也能达到上述我描述的效果。

onBindViewHolder
onBindViewHolder比较简单,发现是HeaderView或者FooterView直接return即可,因为对于头部和底部我们仅仅做展示即可,对于事件应该是在addHeaderView等方法前设置。

这样就初步完成了我们的装饰类,我们分别添加两个headerView和footerView:

大家都知道RecyclerView比较强大,可以设置不同的LayoutManager,那么我们换成GridLayoutMananger再看看效果。



好像发现了不对的地方,我们的headerView果真被当成普通的Item处理了,不过由于我们的编写方式,出现上述情况是可以理解的。

那么我们该如何处理呢?让每个headerView独立的占据一行?

4、进一步的完善

好在RecyclerView里面为我们提供了一些方法。

(1)针对GridLayoutManager

在我们的HeaderAndFooterWrapper中复写onAttachedToRecyclerView方法,如下:

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView)
{
innerAdapter.onAttachedToRecyclerView(recyclerView);

RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager)
{
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
{
@Override
public int getSpanSize(int position)
{
int viewType = getItemViewType(position);
if (mHeaderViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
} else if (mFootViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
}
if (oldLookup != null)
return oldLookup.getSpanSize(position);
return 1;
}
});
gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
}
}



当发现layoutManager为GridLayoutManager时,通过设置SpanSizeLookup,对其getSpanSize方法,返回值设置为layoutManager.getSpanCount();


(2)对于StaggeredGridLayoutManager

在刚才的代码中我们好像没有发现StaggeredGridLayoutManager的身影,StaggeredGridLayoutManager并没有setSpanSizeLookup这样的方法,那么该如何处理呢?

依然不复杂,重写onViewAttachedToWindow方法,如下:

@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
{
mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderViewPos(position) || isFooterViewPos(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams)
{

StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;

p.setFullSpan(true);
}
}
}



这样就完成了对StaggeredGridLayoutManager的处理,效果图就不贴了。

到此,我们就完成了整个HeaderAndFooterWrapper的编写,可以在不改变原Adapter代码的情况下,为其添加一个或者多个headerView或者footerView,以及完成了如何让HeaderView或者FooterView适配各种LayoutManager。

源码地址:https://github.com/hongyangAndroid/baseAdapter
————————————————
版权声明:本文为CSDN博主「鸿洋_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/lmj623565791/article/details/51854533

收起阅读 »

小谈 Kotlin 的空处理

近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin 与 Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。 ...
继续阅读 »

近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。



一、上手的确容易



先扯一扯 Kotlin 学习本身。

之前各种听人说上手容易,但真要切换到另一门语言,难免还是会踌躇是否有这个必要。现在因为工作关系直接上手 Kotlin,感受是 真香(上手的确容易)

首先在代码阅读层面,对于有 Java 基础的程序员来说阅读 Kotlin 代码基本无障碍,除去一些操作符、一些顺序上的变化,整体上可以直接阅读。

其次在代码编写层面,仅需要改变一些编码习惯。主要是:语句不要写分号、变量需要用 var val 声明、类型写在变量之后、实例化一个对象时不用 "new" …… 习惯层面的改变只需要多写代码,自然而然就适应了。

最后在学习方式层面,由于 Kotlin 最终都会被编译成字节码跑在 JVM
上,所以初入手时完全可以用 Java 作为对比。比如你可能不知道
Kotlin
companion object 是什么意思,但你知道既然
Kotlin
最终会转成 jvm 可以跑的字节码,那 Java 里必然可以找到与之对应的东西。

Android Studio 也提供了很方便的工具。选择菜单 Tools -> Kotlin -> Show
Kotlin Bytecode
即可看到 Kotlin 编译成的字节码,点击窗口上方的 "Decompile" 即可看到这份字节码对应的 Java 代码。
—— 这个工具特别重要,假如一段
Kotlin
代码让你看得云里雾里,看一下它对应的 Java 代码你就能知道它的含义。


当然这里仅仅是说上手或入门(仅入门的话可以忽略诸如协程等高级特性),真正熟练应用乃至完全掌握肯定需要一定时间。



二、针对 NPE 的强规则



有些文章说 Kotlin 帮开发者解决了 NPENullPointerException),这个说法是不对的。在我看来,Kotlin 没有帮开发者解决了 NPE Kotlin: 臣妾真的做不到啊),而是通过在语言层面增加各种强规则,强制开发者去自己处理可能的空指针问题,达到尽量减少(只能减少而无法完全避免)出现 NPE 的目的。

那么 Kotlin 具体是怎么做的呢?别着急,我们可以先回顾一下在 Java 中我们是怎么处理空指针问题的。

Java 中对于空指针的处理总体来说可以分为防御式编程契约式编程两种方案。

防御式编程大家应该不陌生,核心思想是不信任任何外部输入 —— 不管是真实的用户输入还是其他模块传入的实参,具体点就是各种判空。创建一个方法需要判空,创建一个逻辑块需要判空,甚至自己的代码内部也需要判空(防止对象的回收之类的)。示例如下:

   public void showToast(Activity activity) {

       if (activity == null) {

           return;

       }

       

       ......

   }

另一种是契约式编程,各个模块之间约定好一种规则,大家按照规则来办事,出了问题找没有遵守规则的人负责,这样可以避免大量的判空逻辑。Android 提供了相关的注解以及最基础的检查来协助开发者,示例如下:

   public void showToast(@NonNull Activity activity) {

       ......

   }

在示例中我们给 Activity 增加了 @NonNull 的注解,就是向所有调用这个方法的人声明了一个约定,调用方应该保证传入的 activity 非空。当然聪明的你应该知道,这是一个很弱的限制,调用方没注意或者不理会这个注解的话,程序就依然还有 NPE 导致的 crash 的风险。

回过头来,对于 Kotlin,我觉得就是一种把契约式编程和防御式编程相结合且提升到语言层面的处理方式。(听起来似乎比 Java 中各种判空或注解更麻烦?继续看下去,你会发现的确是更麻烦……

Kotlin 中,有以下几方面约束:

1.  
在声明阶段,变量需要决定自己是否可为空,比如
var time: Long? 可接受 null,而 var time: Long 则不能接受 null

2.  
在变量传递阶段,必须保持可空性一致,比如形参声明是不为空的,那么实参必须本身是非空或者转为非空才能正常传递。示例如下:

   fun main() {

       ......

       //  test(isOpen) 直接这样调用,编译不通过


       // 可以是在空检查之内传递,证明自己非空


       isOpen?.apply { 

           test(this)

       }

       // 也可以是强制转成非空类型


       test(isOpen!!)

   }





   private fun test(open: Boolean) {

       ......

   }

3.  
在使用阶段,需要严格判空:

   var time: Long? = 1000

     //尽管你才赋值了非空的值,但在使用过程中,你无法这样:


     //time.toInt()

     //必须判空


     time?.toInt()

总的来说 Kotlin 为了解决 NPE 做了大量语言层级的强限制,的确可以做到减少 NPE 的发生。但这种既契约式(判空)又防御式(声明空与非空)的方案会让开发者做更多的工作,会更麻烦一点。

当然,Kotlin 为了减少麻烦,用 "?" 简化了判空逻辑 —— "?" 的实质还是判空,我们可以通过工具查看 time?.toInt() Java 等价代码是:

     if (time != null) {

        int var10000 = (int)time;

     }

这种简化在数据层级很深需要写大量判空语句时会特别方便,这也是为什么虽然逻辑上 Kotlin 让开发者做了更多工作,但写代码过程中却并没有感觉到更麻烦。



三、强规则之下的 NPE 问题



Kotlin 这么严密的防御之下,NPE 问题是否已经被终结了呢?答案当然是否定的。 在实践过程中我们发现主要有以下几种容易导致 NPE 的场景:

1. data class(含义对应
Java
中的 model)声明了非空

例如从后端拿 json 数据的场景,后端的哪个字段可能会传空是客户端无法控制的,这种情况下我们的预期必须是每个字段都可能为空,这样转成 json object 时才不会有问题:

data class User(

       var id: Long?,

       var gender: Long?,

       var avatar: String?)

假如有一个字段忘了加上"?",后端没传该值就会抛出空指针异常。

2. 过分依赖 Kotlin 的空值检查

private lateinit var mUser: User



...



private fun initView() {

   mUser = intent.getParcelableExtra<User>("key_user")

}

Kotlin 的体系中久了会过分依赖于
Android Studio
的空值检查,在代码提示中
Intent
getParcelableExtra 方法返回的是非空,因此这里你直接用方法结果赋值不会有任何警告。但点击进 getParcelableExtra 方法内部你会发现它的实现是这样的:

   public <T extends Parcelable> T getParcelableExtra(String name) {

       return mExtras == null ? null : mExtras.<T>getParcelable(name);

   }

内部的其他代码不展开了,总之它是可能会返回 null 的,直接赋值显然会有问题。

我理解这是 Kotlin 编译工具对 Java 代码检查的不足之处,它无法准确判断 Java 方法是否会返回空就选择无条件信任,即便方法本身可能还声明了 @Nullable

3. 变量或形参声明为非空

这点与第一、第二点都很类似,主要是使用过程中一定要进一步思考传递过来的值是否真的非空。

有人可能会说,那我全部都声明为可空类型不就得了么 —— 这样做会让你在使用该变量的所有地方都需要判空,Kotlin 本身的便利性就荡然无存了。

我的观点是不要因噎废食,使用时多注意点就可以避免大部分问题。

4. !! 强行转为非空

当将可空类型赋值给非空类型时,需要有对空类型的判断,确保非空才能赋值(Kotlin 的约束)。

我们使用!! 可以很方便得将可空转为非空但可空变量值为 null,则会 crash

因此使用上建议在确保非空时才用 !!:

   param!!

否则还是尽量放在判空代码块里:

   param?.let {

      doSomething(it)

   }


四、实践中碰到的问题



Java 的空处理转到 Kotlin 的空处理,我们可能会下意识去寻找对标 Java 的判空写法:

   if (n != null) {

      //非空如何 

   } else {

      //为空又如何


   }

Kotlin 中类似的写法的确有,那就是结合高阶函数 letapplyrun …… 来处理判空,比如上述 Java 代码就可以写成:

   n?.let {

      //非空如何


   } ?: let {

      //为空又如何


   }

但这里有几个小坑。

1. 两个代码块不是互斥关系

假如是 Java 的写法,那么不管 n 的值怎样,两个代码块都是互斥的,也就是非黑即白。但 Kotlin 的这种写法不是(不确定这种写法是否是最佳实践,假如有更好的方案可以留言指出)。

?: 这个操作符可以理解为 if (a != null) a else b,也就是它之前的值非空返回之前的值,否则返回之后的值。

假如用的是 let, 注意看它的返回值是指定 return 或函数里最后一行,那么碰到以下情况:

   val n = 1

   var a = 0

   n?.let {

      a++

      ...

      null  //最后一行为 null


   } ?: let {

      a++

   }

你会很神奇地发现 a 的值是 2,也就是既执行了前一个代码块,也执行了后一个代码块

上面这种写法你可能不以为然,因为很明显地提醒了诸位需要注意最后一行,但假如是之前没注意这个细节或者是下面这种写法呢?

   n?.let {

      ...

      anMap.put(key, value) // anMap 是一个 HashMap


   } ?: let {

      ...

   }

应该很少人会注意到 Map put 方法是有返回值的,且可能会返回 null。那么这种情况下很容易踩坑。

2. 两个代码块的对象不同

let 为例,在 let 代码块里可以用 it 指代该对象(其他高阶函数可能用
this
,类似的),那么我们在写如下代码时可能会顺手这样写:

   activity {

      n?.let {

         it.hashCode() // it n


      } ?: let {

         it.hashCode() // it activity


      }  

   }

结果自然会发现值不一样。前一个代码块 it 指代的是 n,而后一个代码块里 it 指代的是整个代码块指向的
this

原因是 ?: let 之间是没有 . 的,也就是说后一个代码块调用 let 的对象并不是被判空的对象,而是 this。(不过这种场景会出错的概率不大,因为在后一个代码块里很多对象 n 的方法用不了,就会注意到问题了)



后记



总的来说切换到 Kotlin 还是比预期顺利和舒服,写惯了
Kotlin
后再回去写 Java 反倒有点不习惯。今天先写这点,后面有其他需要总结的再分享。


收起阅读 »

Activity启动流程

Activity启动流程很多文章都已经说过了,这里说一下自己的理解。Activity启动流程分两种,一种是启动正在运行的app的Activity,即启动子Activity。如无特殊声明默认和启动该activity的activity处于同一进程。如果有声明在一个...
继续阅读 »

Activity启动流程很多文章都已经说过了,这里说一下自己的理解。

Activity启动流程分两种,一种是启动正在运行的appActivity,即启动子Activity如无特殊声明默认和启动该activityactivity处于同一进程。如果有声明在一个新的进程中,则处于两个进程。另一种是打开新的app,即为Launcher启动新的Activity后边启动Activity的流程是一样的,区别是前边判断进程是否存在的那部分。

Activity启动的前提是已经开机,各项进程和AMS等服务已经初始化完成,在这里也提一下那些内容。

Activity启动之前

init进程init是所有linux程序的起点,是Zygote的父进程。解析init.rc孵化出Zygote进程。

Zygote进程Zygote是所有Java进程的父进程,所有的App进程都是由Zygote进程fork生成的。

SystemServer进程System
Server
Zygote孵化的第一个进程。SystemServer负责启动和管理整个Java framework,包含AMSPMS等服务。

LauncherZygote进程孵化的第一个App进程是Launcher

1.init进程是什么?

Android是基于linux系统的,手机开机之后,linux内核进行加载。加载完成之后会启动init进程。

init
进程会启动ServiceManager,孵化一些守护进程,并解析init.rc孵化Zygote进程。

2.Zygote进程是什么?

所有的App进程都是由Zygote进程fork生成的,包括SystemServer进程。Zygote初始化后,会注册一个等待接受消息的socketOS层会采用socket进行IPC通信。

3.为什么是Zygote来孵化进程,而不是新建进程呢?

每个应用程序都是运行在各自的Dalvik虚拟机中,应用程序每次运行都要重新初始化和启动虚拟机,这个过程会耗费很长时间。Zygote会把已经运行的虚拟机的代码和内存信息共享,起到一个预加载资源和类的作用,从而缩短启动时间。

Activity启动阶段

涉及到的概念

进程Android系统为每个APP分配至少一个进程

IPC:跨进程通信,Android中采用Binder机制。

涉及到的类

ActivityStackActivityAMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。

ActivitySupervisor:管理 activity 任务栈

ActivityThreadActivityThread
运行在UI线程(主线程),App的真正入口。

ApplicationThread:用来实现AMSActivityThread之间的交互。

ApplicationThreadProxyApplicationThread
在服务端的代理。AMS就是通过该代理与ActivityThread进行通信的。

IActivityManager:继承与IInterface接口,抽象出跨进程通信需要实现的功能

AMN:运行在server端(SystemServer进程)。实现了Binder类,具体功能由子类AMS实现。

AMSAMN的子类,负责管理四大组件和进程,包括生命周期和状态切换。AMS因为要和ui交互,所以极其复杂,涉及window

AMPAMSclient端代理(app进程)。了解Binder知识可以比较容易理解server端的stubclient端的proxyAMPAMS通过Binder通信。

Instrumentation:仪表盘,负责调用ActivityApplication生命周期。测试用到这个类比较多。

流程图

这个图来源自网上,之前也看过很多类似讲流程的文章,但是大都是片段的。这个图是目前看到的最全的,自己去画一下也应该不会比这个全了,所以在这里直接引用一下,可以去浏览器上放大看。


涉及到的进程

·      
Launcher所在的进程

·      
AMS所在的SystemServer进程

·      
要启动的Activity所在的app进程

如果是启动根Activity,就涉及上述三个进程。

如果是启动子Activity,那么就只涉及AMS进程和app所在进程。

具体流程

1. LauncherLauncher通知AMS要启动activity

·      
startActivitySafely->startActivity->Instrumentation.execStartActivity()(AMP.startActivity)->AMS.startActivity

2. AMS:PMSresoveIntent验证要启动activity是否匹配。如果匹配,通过ApplicationThread发消息给Launcher所在的主线程,暂停当前Activity(Launcher)

3. 暂停完,在该activity还不可见时,通知AMS,根据要启动的Activity配置ActivityStack。然后判断要启动的Activity进程是否存在?

·      
存在:发送消息LAUNCH_ACTIVITY给需要启动的Activity主线程,执行handleLaunchActivity

·      
不存在:通过socketzygote请求创建进程。进程启动后,ActivityThread.attach

4. 判断Application是否存在,若不存在,通过LoadApk.makeApplication创建一个。在主线程中通过thread.attach方法来关联ApplicationThread

5. 在通过ActivityStackSupervisor来获取当前需要显示的ActivityStack

6. 继续通过ApplicationThread来发送消息给主线程的Handler来启动ActivityhandleLaunchActivity)。

7. handleLauchActivity:调用了performLauchActivity,里边Instrumentation生成了新的activity对象,继续调用activity生命周期。

IPC过程:

双方都是通过对方的代理对象来进行通信。

1.app
AMS通信:app通过本进程的AMPAMS进行Binder通信

2.AMS
和新app通信:通过ApplicationThreadProxy来通信,并不直接和ActivityThread通信

参考函数流程

Activity启动流程(从Launcher开始):

第一阶段: Launcher通知AMS要启动新的Activity(在Launcher所在的进程执行)

·      
Launcher.startActivitySafely //首先Launcher发起启动Activity的请求

·      
Activity.startActivity

·      
Activity.startActivityForResult

·      
Instrumentation.execStartActivity //交由Instrumentation代为发起请求

·      
ActivityManager.getService().startActivity
//
通过IActivityManagerSingleton.get()得到一个AMP代理对象

·      
ActivityManagerProxy.startActivity
//
通过AMP代理通知AMS启动activity

第二阶段:AMS先校验一下Activity的正确性,如果正确的话,会暂存一下Activity的信息。然后,AMS会通知Launcher程序pause Activity(在AMS所在进程执行)

·      
ActivityManagerService.startActivity

·      
ActivityManagerService.startActivityAsUser

·      
ActivityStackSupervisor.startActivityMayWait

·      
ActivityStackSupervisor.startActivityLocked
:检查有没有在AndroidManifest中注册

·      
ActivityStackSupervisor.startActivityUncheckedLocked

·      
ActivityStack.startActivityLocked :判断是否需要创建一个新的任务来启动Activity

·      
ActivityStack.resumeTopActivityLocked :获取栈顶的activity,并通知Launcher应该pause掉这个Activity以便启动新的activity

·      
ActivityStack.startPausingLocked

·      
ApplicationThreadProxy.schedulePauseActivity

第三阶段: pause LauncherActivity,并通知AMS已经paused(在Launcher所在进程执行)

·      
ApplicationThread.schedulePauseActivity

·      
ActivityThread.queueOrSendMessage

·      
H.handleMessage

·      
ActivityThread.handlePauseActivity

·      
ActivityManagerProxy.activityPaused

第四阶段:检查activity所在进程是否存在,如果存在,就直接通知这个进程,在该进程中启动Activity;不存在的话,会调用Process.start创建一个新进程(执行在AMS进程)

·      
ActivityManagerService.activityPaused

·      
ActivityStack.activityPaused

·      
ActivityStack.completePauseLocked

·      
ActivityStack.resumeTopActivityLocked

·      
ActivityStack.startSpecificActivityLocked

·      
ActivityManagerService.startProcessLocked

·      
Process.start //在这里创建了新进程,新的进程会导入ActivityThread类,并执行它的main函数

第五阶段: 创建ActivityThread实例,执行一些初始化操作,并绑定Application。如果Application不存在,会调用LoadedApk.makeApplication创建一个新的Application对象。之后进入Loop循环。(执行在新创建的app进程)

·      
ActivityThread.main

·      
ActivityThread.attach(false) //声明不是系统进程

·      
ActivityManagerProxy.attachApplication

第六阶段:处理新的应用进程发出的创建进程完成的通信请求,并通知新应用程序进程启动目标Activity组件(执行在AMS进程)

·      
ActivityManagerService.attachApplication //AMS绑定本地ApplicationThread对象,后续通过ApplicationThreadProxy来通信。

·      
ActivityManagerService.attachApplicationLocked

·      
ActivityStack.realStartActivityLocked //真正要启动Activity了!

·      
ApplicationThreadProxy.scheduleLaunchActivity
//AMS
通过ATP通知app进程启动Activity

第七阶段: 加载MainActivity类,调用onCreate声明周期方法(执行在新启动的app进程)

·      
ApplicationThread.scheduleLaunchActivity
//ApplicationThread
发消息给AT

·      
ActivityThread.queueOrSendMessage

·      
H.handleMessage //ATHandler来处理接收到的LAUNCH_ACTIVITY的消息

·      
ActivityThread.handleLaunchActivity

·      
ActivityThread.performLaunchActivity

·      
Instrumentation.newActivity //调用Instrumentation类来新建一个Activity对象

·      
Instrumentation.callActivityOnCreate

·      
MainActivity.onCreate

·      
ActivityThread.handleResumeActivity

·      
AMP.activityResumed

·      
AMS.activityResumed(AMS进程)

参考文章

http://gityuan.com/2016/03/12/start-activity/

https://blog.csdn.net/luoshengyang/article/details/6689748

收起阅读 »

一个Android TabLayout库,目前有3个TabLayout

FlycoTabLayout示例图:一个Android TabLayout库,目前有3个TabLayoutSlidingTabLayout:参照PagerSlidingTabStrip进行大量修改.新增部分属性新增支持多种Indicator显示器新增支持未读消...
继续阅读 »

FlycoTabLayout


示例图:



一个Android TabLayout库,目前有3个TabLayout

  • SlidingTabLayout:参照PagerSlidingTabStrip进行大量修改.

    • 新增部分属性
    • 新增支持多种Indicator显示器
    • 新增支持未读消息显示
    • 新增方法for懒癌患者
        /** 关联ViewPager,用于不想在ViewPager适配器中设置titles数据的情况 */
    public void setViewPager(ViewPager vp, String[] titles)

    /** 关联ViewPager,用于连适配器都不想自己实例化的情况 */
    public void setViewPager(ViewPager vp, String[] titles, FragmentActivity fa, ArrayList<Fragment> fragments)
  • CommonTabLayout:不同于SlidingTabLayout对ViewPager依赖,它是一个不依赖ViewPager可以与其他控件自由搭配使用的TabLayout.

    • 支持多种Indicator显示器,以及Indicator动画
    • 支持未读消息显示
    • 支持Icon以及Icon位置
    • 新增方法for懒癌患者
        /** 关联数据支持同时切换fragments */
    public void setTabData(ArrayList<CustomTabEntity> tabEntitys, FragmentManager fm, int containerViewId, ArrayList<Fragment> fragments)
  • SegmentTabLayout


Gradle

dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.flyco.roundview:FlycoRoundView_Lib:1.1.2@aar'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:1.5.0@aar'
}

After v2.0.0(support 2.2+)
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.0.0@aar'
}

After v2.0.2(support 3.0+)
dependencies{
compile 'com.android.support:support-v4:23.1.1'
compile 'com.flyco.tablayout:FlycoTabLayout_Lib:2.1.2@aar'
}

代码下载:FlycoTabLayout-master.zip

原文链接:https://github.com/H07000223/FlycoTabLayout

收起阅读 »

iOS 实例对象,类对象,元类对象

OC对象的分类OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)实例对象:实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据...
继续阅读 »

OC对象的分类

OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)

  • 实例对象:

    实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:
 NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];

NSLog(@"instance----%p %p",objc1,objc2);
输出结果:



instance实例对象存储的信息:
1.isa指针

2.其他成员变量


我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址

类对象

类对象的获取
        Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

打印结果

2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个

类对象的作用

类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)



元类对象

1.元类对象的获取

        Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);

打印指针地址

NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个

特别注意一点:

Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];

无论class几次,它返回的始终是类对象

2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140

元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)



从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空

是否为元类 class_isMetaClass(objcL);

收起阅读 »

iOS开发 - 编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »
对于平常的应用程序开发,我们很少需要关注编译链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译链接的过程一步完成,通常将这种编译链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余。

编译流程分析

现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World
复制代码

其实上述过程可以分解为四步:

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)



预编译

首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i
复制代码

还可以下面的表达

$ cpp hello.c > hello.i
复制代码

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

  • 将所有的#define删除,并展开所有的宏定义
  • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释///**/
  • 添加行号和文件名标识,比如#2 “hello.c” 2。
  • 保留所有的#pragma编译器指令

截图个大家看看效果

经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)

编译过程就是把预处理完的文件进行一系列的:词法分析语法分析语义分析优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s



通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o
复制代码

或者

gcc –c hello.s –o hello.o
复制代码

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o
复制代码

链接(linking)

  链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc
复制代码

链接相应的库




下面在贴出我们的写出的源代码是如何变成目标代码的流程图:




主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看



iOS的编译器

iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。




编译器前端的任务是进行:

  • 语法分析
  • 语义分析
  • 生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


以上图解内容所做的是事情和gcc编译一模模一样样!

iOS程序-详细编译过程

  • 1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
  • 2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
  • 3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
  • 4.链接文件:将项目中的多个可执行文件合并成一个文件;
  • 5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
  • 6.编译 storyboard 文件:storyboard 文件也是会被编译的;
  • 7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
  • 8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage
  • 9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
  • 10.生成 .app 包
  • 11.将 Swift 标准库拷贝到包中
  • 12.对包进行签名
  • 13.完成打包
编译过程的确是个比较复杂的过程,还有链接!并不是说难就不需要掌握,我个人建议每一个进阶路上iOS开发人员,都是要了解一下的。不需要你多么牛逼,但是你能在平时的交流讨论,面试中能点出一个两个相应的点,我相信绝对是逼格满满!


作者:Cooci
原贴链接:https://juejin.cn/post/6844903841842872327


收起阅读 »

iOS开发 Fastlane 自动打包技术

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布AppGithub官网文档我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们...
继续阅读 »

Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App

Github

官网

文档

我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:

  • 一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。

  • 避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。

  • 通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect。

  • 证书的同步更新,管理,在新电脑能够迅速具备项目打包环境

如果你也有上述需求,那我相信Fastlane是一个好的选择。

那既然说Fastlane是一套工具的集合,那认识并了解其中的工具的作用是必不可少的环节。按照功能属性Fastlane对工具进行了如下分类(链接至官网详细介绍):

Testing 测试相关

Building 打包

Screenshots 截图

Project 项目配置

Code Signing 代码签名

Documentation 文档

Beta 内测相关

Push 推送

Releasing your app 发布

Source Control Git工作流

Notifications 通知相关

Misc 其他的杂七杂八

分类下对应的就是具体的每一个工具的介绍,在这里每一个工具Fastlane叫做action,下文我们也统一叫action。这里我会列举一些我认为常用的action,其他的大家可以去官网看下

gym:是fastlane提供的打包工具

snapshot: 生成多个设备的截图文件

frameit :对截图加一层物理边框

increment_build_number:自增build number 然后与之对应的get_build_number。Version number同理。

cert:创建一个新的代码签名证书

sigh:生成一个provisioning profile并保存打当前文件

pem:确保当前的推送证书是活跃的,如果没有会帮你生成一个新的

match:在团队中同步证书和描述文件。(这是一种管理证书的全新方式,需要重点关注下)

testflight:上传ipa到testflight

deliver:上传ipa到AppStore

当然官网里面其实是有很多可以划等号的Action,大家在看的时候注意下。Actions官网关于Action的介绍

多说无益,开始上手

当前最新版本是2.8.4

一、安装xcode命令行工具

xcode-select --install,如果没有安装,会弹出对话框,点击安装。如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装

二、安装Fastlane

sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的

安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。

三、初始化Fastlane

cd到你的项目目录执行 

fastlane init


这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。

如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。



接着会提示你输入开发者账号和密码。

[20:48:55]: Please enter your Apple ID developer credentials

[20:48:55]: Apple ID Username:

登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。


四、文件系统

初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。


其中metadata和screenshots分别对应App元数据和商店应用截图。

Appfile主要存放App的apple_id team_id app_identifier等信息

Deliverfile中为发布的配置信息,一般情况用不到。

Fastfile是我们最应该关注的文件,也是我们的工作文件。

Fastfile



之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。

打包到蒲公英

这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。

蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。

打开终端输入fastlane add_plugin pgyer


新建一个lane


desc "打包到pgy"


lane :test do |options|
gym(
clean:true, #打包前clean项目
export_method: "ad-hoc", #导出方式
scheme:"shangshaban", #scheme
configuration: "Debug",#环境
output_directory:"./app",#ipa的存放目录
output_name:get_build_number()#输出ipa的文件名为当前的build号
)
#蒲公英的配置 替换为自己的api_key和user_key
pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
end

这样一个打包到蒲公英的lane就完成了。

option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台

执行

在工作目录的终端执行

fastlane test desc:测试打包



然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置

其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解。

match

开头已经说了,match是一种全新的证书同步管理机制,也是我认为在fastlane中相对重要的一环,介于篇幅这篇就不细说了,有兴趣的可以去官网看下,有机会我也会更新一篇关于match的文章。match

其他的一些小提示

  1. 可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来

  2. 如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。

  3. 如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。

  4. 如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据skip_screenshots:true,#不上传屏幕截图

关于fastlane的一些想法

其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!



作者:Cooci
原贴链接:https://www.jianshu.com/p/59725c52e0fa





收起阅读 »

FloatWindow 安卓任意界面悬浮窗

效果图:特性:1.支持拖动,提供自动贴边等动画2.内部自动进行权限申请操作3.可自由指定要显示悬浮窗的界面4.应用退到后台时,悬浮窗会自动隐藏5.除小米外,4.4~7.0 无需权限申请6.位置及宽高可设置百分比值,轻松适配各分辨率7.支持权限申请结果、位置等状...
继续阅读 »

效果图:



特性:

1.支持拖动,提供自动贴边等动画

2.内部自动进行权限申请操作

3.可自由指定要显示悬浮窗的界面

4.应用退到后台时,悬浮窗会自动隐藏

5.除小米外,4.4~7.0 无需权限申请

6.位置及宽高可设置百分比值,轻松适配各分辨率

7.支持权限申请结果、位置等状态监听

8.链式调用,简洁清爽

集成:

第 1 步、在工程的 build.gradle 中添加:

	allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

第 2 步、在应用的 build.gradle 中添加:

	dependencies {
compile 'com.github.yhaolpz:FloatWindow:1.0.9'
}

使用:

0.声明权限

     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

1.基础使用

        FloatWindow
.with(getApplicationContext())
.setView(view)
.setWidth(100) //设置控件宽高
.setHeight(Screen.width,0.2f)
.setX(100) //设置控件初始位置
.setY(Screen.height,0.3f)
.setDesktopShow(true) //桌面显示
.setViewStateListener(mViewStateListener) //监听悬浮控件状态改变
.setPermissionListener(mPermissionListener) //监听权限申请结果
.build();

宽高及位置可设像素值或屏幕宽/高百分比,默认宽高为 wrap_content;默认位置为屏幕左上角,x、y 为偏移量。

2.指定界面显示

              .setFilter(true, A_Activity.class, C_Activity.class)

此方法表示 A_Activity、C_Activity 显示悬浮窗,其他界面隐藏。

              .setFilter(false, B_Activity.class)

此方法表示 B_Activity 隐藏悬浮窗,其他界面显示。

注意:setFilter 方法参数可以识别该 Activity 的子类

也就是说,如果 A_Activity、C_Activity 继承自 BaseActivity,你可以这样设置:

              .setFilter(true, BaseActivity.class)

3.可拖动悬浮窗及回弹动画

              .setMoveType(MoveType.slide)
.setMoveStyle(500, new AccelerateInterpolator()) //贴边动画时长为500ms,加速插值器

共提供 4 种 MoveType :

MoveType.slide : 可拖动,释放后自动贴边 (默认)

MoveType.back : 可拖动,释放后自动回到原位置

MoveType.active : 可拖动

MoveType.inactive : 不可拖动

setMoveStyle 方法可设置动画效果,只在 MoveType.slide 或 MoveType.back 模式下设置此项才有意义。默认减速插值器,默认动画时长为 300ms。

4.后续操作

        //手动控制
FloatWindow.get().show();
FloatWindow.get().hide();

//修改显示位置
FloatWindow.get().updateX(100);
FloatWindow.get().updateY(100);

//销毁
FloatWindow.destroy();

以上操作应待悬浮窗初始化后进行。

5.多个悬浮窗

        FloatWindow
.with(getApplicationContext())
.setView(imageView)
.build();

FloatWindow
.with(getApplicationContext())
.setView(button)
.setTag("new")
.build();


FloatWindow.get("new").show();
FloatWindow.get("new").hide();
FloatWindow.destroy("new");

创建第一个悬浮窗不需加 tag,之后再创建就需指定唯一 tag ,以此区分,方便进行后续操作。


代码下载:FloatWindow-master.zip

原文链接:https://github.com/yhaolpz/FloatWindow

收起阅读 »

iOS 超强富文本编辑库

YYText功能强大的 iOS 富文本编辑与显示框架特性API 兼容 UILabel 和 UITextView支持高性能的异步排版和渲染扩展了 CoreText 的属性以支持更多文字效果支持 UIImage、UIView、CALayer 作为图文混排元素支持添...
继续阅读 »

YYText

功能强大的 iOS 富文本编辑与显示框架

特性

  • API 兼容 UILabel 和 UITextView
  • 支持高性能的异步排版和渲染
  • 扩展了 CoreText 的属性以支持更多文字效果
  • 支持 UIImage、UIView、CALayer 作为图文混排元素
  • 支持添加自定义样式的、可点击的文本高亮范围
  • 支持自定义文本解析 (内置简单的 Markdown/表情解析)
  • 支持文本容器路径、内部留空路径的控制
  • 支持文字竖排版,可用于编辑和显示中日韩文本
  • 支持图片和富文本的复制粘贴
  • 文本编辑时,支持富文本占位符
  • 支持自定义键盘视图
  • 撤销和重做次数的控制
  • 富文本的序列化与反序列化支持
  • 支持多语言,支持 VoiceOver
  • 支持 Interface Builder
  • 全部代码都有文档注释
  • 架构

    YYText 和 TextKit 架构对比



    文本属性

    YYText 原生支持的属性

    DemoAttribute NameClass
    TextAttachmentYYTextAttachment
    TextHighlightYYTextHighlight
    TextBindingYYTextBinding
    TextShadow
    TextInnerShadow
    YYTextShadow
    TextBorderYYTextBorder
    TextBackgroundBorderYYTextBorder
    TextBlockBorderYYTextBorder
    TextGlyphTransformNSValue(CGAffineTransform)
    TextUnderlineYYTextDecoration
    TextStrickthroughYYTextDecoration
    TextBackedStringYYTextBackedString

    YYText 支持的 CoreText 属性

    DemoAttribute NameClass
    Font UIFont(CTFontRef)
    Kern NSNumber
    StrokeWidth NSNumber 
    StrokeColor CGColorRef 
    Shadow NSShadow 
    Ligature NSNumber 
    VerticalGlyphForm NSNumber(BOOL) 
    WritingDirection NSArray(NSNumber) 
    RunDelegate CTRunDelegateRef 
    TextAlignment NSParagraphStyle 
    (NSTextAlignment) 
    LineBreakMode NSParagraphStyle 
    (NSLineBreakMode) 
    LineSpacing NSParagraphStyle 
    (CGFloat) 
    ParagraphSpacing 
    ParagraphSpacingBefore 
    NSParagraphStyle 
    (CGFloat) 
    FirstLineHeadIndent NSParagraphStyle 
    (CGFloat) 
    HeadIndent NSParagraphStyle 
    (CGFloat) 
    TailIndent NSParagraphStyle 
    (CGFloat) 
    MinimumLineHeight NSParagraphStyle 
    (CGFloat) 
    MaximumLineHeight NSParagraphStyle 
    (CGFloat) 
    LineHeightMultiple NSParagraphStyle 
    (CGFloat) 
    BaseWritingDirection NSParagraphStyle 
    (NSWritingDirection) 
    DefaultTabInterval 
    TabStops 
    NSParagraphStyle 
    CGFloat/NSArray(NSTextTab)



    用法

    基本用法

    // YYLabel (和 UILabel 用法一致)
    YYLabel *label = [YYLabel new];
    label.frame = ...
    label.font = ...
    label.textColor = ...
    label.textAlignment = ...
    label.lineBreakMode = ...
    label.numberOfLines = ...
    label.text = ...

    // YYTextView (和 UITextView 用法一致)
    YYTextView *textView = [YYTextView new];
    textView.frame = ...
    textView.font = ...
    textView.textColor = ...
    textView.dataDetectorTypes = ...
    textView.placeHolderText = ...
    textView.placeHolderTextColor = ...
    textView.delegate = ...



    属性文本

    // 1. 创建一个属性文本
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:@"Some Text, blabla..."];

    // 2. 为文本设置属性
    text.yy_font = [UIFont boldSystemFontOfSize:30];
    text.yy_color = [UIColor blueColor];
    [text yy_setColor:[UIColor redColor] range:NSMakeRange(0, 4)];
    text.yy_lineSpacing = 10;

    // 3. 赋值到 YYLabel 或 YYTextView
    YYLabel *label = [YYLabel new];
    label.frame = ...
    label.attributedString = text;

    YYTextView *textView = [YYTextView new];
    textView.frame = ...
    textView.attributedString = text;



    文本高亮

    你可以用一些已经封装好的简便方法来设置文本高亮:

    [text yy_setTextHighlightRange:range
    color:[UIColor blueColor]
    backgroundColor:[UIColor grayColor]
    tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
    NSLog(@"tap text range:...");
    }];

    文本高亮

    你可以用一些已经封装好的简便方法来设置文本高亮:

    [text yy_setTextHighlightRange:range
    color:[UIColor blueColor]
    backgroundColor:[UIColor grayColor]
    tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
    NSLog(@"tap text range:...");
    }];

    或者用更复杂的办法来调节文本高亮的细节:

    // 1. 创建一个"高亮"属性,当用户点击了高亮区域的文本时,"高亮"属性会替换掉原本的属性
    YYTextBorder *border = [YYTextBorder borderWithFillColor:[UIColor grayColor] cornerRadius:3];

    YYTextHighlight *highlight = [YYTextHighlight new];
    [highlight setColor:[UIColor whiteColor]];
    [highlight setBackgroundBorder:highlightBorder];
    highlight.tapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
    NSLog(@"tap text range:...");
    // 你也可以把事件回调放到 YYLabel 和 YYTextView 来处理。
    };

    // 2. 把"高亮"属性设置到某个文本范围
    [attributedText yy_setTextHighlight:highlight range:highlightRange];

    // 3. 把属性文本设置到 YYLabel 或 YYTextView
    YYLabel *label = ...
    label.attributedText = attributedText

    YYTextView *textView = ...
    textView.attributedText = ...

    // 4. 接受事件回调
    label.highlightTapAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
    NSLog(@"tap text range:...");
    };
    label.highlightLongPressAction = ^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect) {
    NSLog(@"long press text range:...");
    };

    @UITextViewDelegate
    - (void)textView:(YYTextView *)textView didTapHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
    NSLog(@"tap text range:...");
    }
    - (void)textView:(YYTextView *)textView didLongPressHighlight:(YYTextHighlight *)highlight inRange:(NSRange)characterRange rect:(CGRect)rect {
    NSLog(@"long press text range:...");
    }

    图文混排

    NSMutableAttributedString *text = [NSMutableAttributedString new];
    UIFont *font = [UIFont systemFontOfSize:16];
    NSMutableAttributedString *attachment = nil;

    // 嵌入 UIImage
    UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"];
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [text appendAttributedString: attachment];

    // 嵌入 UIView
    UISwitch *switcher = [UISwitch new];
    [switcher sizeToFit];
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [text appendAttributedString: attachment];

    // 嵌入 CALayer
    CASharpLayer *layer = [CASharpLayer layer];
    layer.path = ...
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [text appendAttributedString: attachment];


    文本布局计算

    NSAttributedString *text = ...
    CGSize size = CGSizeMake(100, CGFLOAT_MAX);
    YYTextLayout *layout = [YYTextLayout layoutWithContainerSize:size text:text];

    // 获取文本显示位置和大小
    layout.textBoundingRect; // get bounding rect
    layout.textBoundingSize; // get bounding size

    // 查询文本排版结果
    [layout lineIndexForPoint:CGPointMake(10,10)];
    [layout closestLineIndexForPoint:CGPointMake(10,10)];
    [layout closestPositionToPoint:CGPointMake(10,10)];
    [layout textRangeAtPoint:CGPointMake(10,10)];
    [layout rectForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];
    [layout selectionRectsForRange:[YYTextRange rangeWithRange:NSMakeRange(10,2)]];

    // 显示文本排版结果
    YYLabel *label = [YYLabel new];
    label.size = layout.textBoundingSize;
    label.textLayout = layout;

    部分效果展示






    安装

    CocoaPods

    1. 在 Podfile 中添加 pod 'YYText'
    2. 执行 pod install 或 pod update
    3. 导入

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYText"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入

    手动安装

    1. 下载 YYText 文件夹内的所有内容。
    2. 将 YYText 内的源文件添加(拖放)到你的工程。
    3. 链接以下 frameworks:
      • UIKit
      • CoreFoundation
      • CoreText
      • QuartzCore
      • Accelerate
      • MobileCoreServices
    4. 导入 YYText.h


    已知问题

  • YYText 并不能支持所有 CoreText/TextKit 的属性,比如 NSBackgroundColor、NSStrikethrough、NSUnderline、NSAttachment、NSLink 等,但 YYText 中基本都有对应属性作为替代。详情见上方表格。
  • YYTextView 未实现局部刷新,所以在输入和编辑大量的文本(比如超过大概五千个汉字、或大概一万个英文字符)时会出现较明显的卡顿现象。
  • 竖排版时,添加 exclusionPaths 在少数情况下可能会导致文本显示空白。
  • 当添加了非矩形的 textContainerPath,并且有嵌入大于文本排版方向宽度的 RunDelegate 时,RunDelegate 之后的文字会无法显示。这是 CoreText 的 Bug(或者说是 Feature)。

  • 常见问题及源码下载:点击这里

    demo:YYText.zip


    收起阅读 »

    iOS 数据缓存库

    YYCache高性能 iOS 缓存框架。特性LRU: 缓存支持 LRU (least-recently-used) 淘汰算法。缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。兼容性: API 基本和 NSCache 保...
    继续阅读 »

    YYCache

    高性能 iOS 缓存框架。

    特性

  • LRU: 缓存支持 LRU (least-recently-used) 淘汰算法。
  • 缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。
  • 兼容性: API 基本和 NSCache 保持一致, 所有方法都是线程安全的。
  • 内存缓存
    • 对象释放控制: 对象的释放(release) 可以配置为同步或异步进行,可以配置在主线程或后台线程进行。
    • 自动清空: 当收到内存警告或 App 进入后台时,缓存可以配置为自动清空。
  • 磁盘缓存
    • 可定制性: 磁盘缓存支持自定义的归档解档方法,以支持那些没有实现 NSCoding 协议的对象。
    • 存储类型控制: 磁盘缓存支持对每个对象的存储类型 (SQLite/文件) 进行自动或手动控制,以获得更高的存取性能。

  • 安装

    CocoaPods

    1. 在 Podfile 中添加 pod 'YYCache'
    2. 执行 pod install 或 pod update
    3. 导入

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYCache"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入

    手动安装

    1. 下载 YYCache 文件夹内的所有内容。
    2. 将 YYCache 内的源文件添加(拖放)到你的工程。
    3. 链接以下的 frameworks:
      • UIKit
      • CoreFoundation
      • QuartzCore
      • sqlite3
    4. 导入 YYCache.h


    常见问题与源码下载:点击这里

    代码示例:YYCache.zip






    收起阅读 »

    iOS 强大的图片处理库(webP.编解码.gif)等

    YYImage支持以下类型动画图像的播放/编码/解码:    WebP, APNG, GIF。支持以下类型静态图像的显示/编码/解码:    WebP, PNG, GIF, JPE...
    继续阅读 »

    YYImage

  • 支持以下类型动画图像的播放/编码/解码:
        WebP, APNG, GIF。
  • 支持以下类型静态图像的显示/编码/解码:
        WebP, PNG, GIF, JPEG, JP2, TIFF, BMP, ICO, ICNS。
  • 支持以下类型图片的渐进式/逐行扫描/隔行扫描解码:
        PNG, GIF, JPEG, BMP。
  • 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画。
  • 高效的动态内存缓存管理,以保证高性能低内存的动画播放。
  • 完全兼容 UIImage 和 UIImageView,使用方便。
  • 保留可扩展的接口,以支持自定义动画。
  • 每个类和方法都有完善的文档注释。

  • 安装

    CocoaPods

    1. 将 cocoapods 更新至最新版本.
    2. 在 Podfile 中添加 pod 'YYImage'
    3. 执行 pod install 或 pod update
    4. 导入
    5. 注意:pod 配置并没有包含 WebP 组件, 如果你需要支持 WebP,可以在 Podfile 中添加 pod 'YYImage/WebP'

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYImage"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入
    4. 注意:carthage framework 并没有包含 WebP 组件。如果你需要支持 WebP,可以用 CocoaPods 安装,或者手动安装。

    手动安装

    1. 下载 YYImage 文件夹内的所有内容。
    2. 将 YYImage 内的源文件添加(拖放)到你的工程。
    3. 链接以下 frameworks:
      • UIKit
      • CoreFoundation
      • QuartzCore
      • AssetsLibrary
      • ImageIO
      • Accelerate
      • MobileCoreServices
      • libz
    4. 导入 YYImage.h
    5. 注意:如果你需要支持 WebP,可以将 Vendor/WebP.framework(静态库) 加入你的工程。

    用法

    显示动画类型的图片

    // 文件: ani@3x.gif
    UIImage *image = [YYImage imageNamed:@"ani.gif"];
    UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];

    播放帧动画

    // 文件: frame1.png, frame2.png, frame3.png
    NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
    NSArray *times = @[@0.1, @0.2, @0.1];
    UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
    UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];

    动画播放控制

    YYAnimatedImageView *imageView = ...;
    // 暂停:
    [imageView stopAnimating];
    // 播放:
    [imageView startAnimating];
    // 设置播放进度:
    imageView.currentAnimatedImageIndex = 12;
    // 获取播放状态:
    image.currentIsPlayingAnimation;
    //上面两个属性都支持 KVO。

    图片解码

    // 解码单帧图片:
    NSData *data = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;

    // 渐进式图片解码 (可用于图片下载显示):
    NSMutableData *data = [NSMutableData new];
    YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
    while(newDataArrived) {
    [data appendData:newData];
    [decoder updateData:data final:NO];
    if (decoder.frameCount > 0) {
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
    // progressive display...
    }
    }
    [decoder updateData:data final:YES];
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
    // final display...


    图片编码

    // 编码静态图 (支持各种常见图片格式):
    YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
    jpegEncoder.quality = 0.9;
    [jpegEncoder addImage:image duration:0];
    NSData jpegData = [jpegEncoder encode];

    // 编码动态图 (支持 GIF/APNG/WebP):
    YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
    webpEncoder.loopCount = 5;
    [webpEncoder addImage:image0 duration:0.1];
    [webpEncoder addImage:image1 duration:0.15];
    [webpEncoder addImage:image2 duration:0.2];
    NSData webpData = [webpEncoder encode];

    图片类型探测

    // 获取图片类型
    YYImageType type = YYImageDetectType(data);
    if (type == YYImageTypePNG) ...

    常见问题及源码:点击这里

    demo下载:YYImage.zip


    收起阅读 »

    iOS JSON转换库

    YYModel特性高性能: 模型转换性能接近手写解析代码。自动类型转换: 对象类型可以自动转换,详情见下方表格。类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。无侵入性: 模型无需继承自其他基类。轻量: 该框架只有 5 个文...
    继续阅读 »

    YYModel

    特性

    • 高性能: 模型转换性能接近手写解析代码。
    • 自动类型转换: 对象类型可以自动转换,详情见下方表格。
    • 类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
    • 无侵入性: 模型无需继承自其他基类。
    • 轻量: 该框架只有 5 个文件 (包括.h文件)。
    • 文档和单元测试: 文档覆盖率100%, 代码覆盖率99.6%


    使用方法

    简单的 Model 与 JSON 相互转换

    // JSON:
    {
    "uid":123456,
    "name":"Harry",
    "created":"1965-07-31T00:00:00+0000"
    }

    // Model:
    @interface User : NSObject
    @property UInt64 uid;
    @property NSString *name;
    @property NSDate *created;
    @end
    @implementation User
    @end


    // 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
    User *user = [User yy_modelWithJSON:json];

    // 将 Model 转换为 JSON 对象:
    NSDictionary *json = [user yy_modelToJSONObject];

    当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。


    JSON/DictionaryModel
    NSStringNSNumber,NSURL,SEL,Class
    NSNumberNSString
    NSString/NSNumber基础类型 (BOOL,int,float,NSUInteger,UInt64,...)
    NaN 和 Inf 会被忽略
    NSStringNSDate 以下列格式解析:
    yyyy-MM-dd
    yyyy-MM-dd HH:mm:ss
    yyyy-MM-dd'T'HH:mm:ss
    yyyy-MM-dd'T'HH:mm:ssZ
    EEE MMM dd HH:mm:ss Z yyyy
    NSDateNSString 格式化为 ISO8601:
    "YYYY-MM-dd'T'HH:mm:ssZ"
    NSValuestruct (CGRect,CGSize,...)
    NSNullnil,0
    "no","false",...@(NO),0
    "yes","true",...@(YES),1



    Model 属性名和 JSON 中的 Key 不相同

    // JSON:
    {
    "n":"Harry Pottery",
    "p": 256,
    "ext" : {
    "desc" : "A book written by J.K.Rowing."
    },
    "ID" : 100010
    }

    // Model:
    @interface Book : NSObject
    @property NSString *name;
    @property NSInteger page;
    @property NSString *desc;
    @property NSString *bookID;
    @end
    @implementation Book
    //返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。
    + (NSDictionary *)modelCustomPropertyMapper {
    return @{@"name" : @"n",
    @"page" : @"p",
    @"desc" : @"ext.desc",
    @"bookID" : @[@"id",@"ID",@"book_id"]};
    }
    @end


    你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。

    在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。

    在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。

    Model 包含其他 Model

    // JSON
    {
    "author":{
    "name":"J.K.Rowling",
    "birthday":"1965-07-31T00:00:00+0000"
    },
    "name":"Harry Potter",
    "pages":256
    }

    // Model: 什么都不用做,转换会自动完成
    @interface Author : NSObject
    @property NSString *name;
    @property NSDate *birthday;
    @end
    @implementation Author
    @end

    @interface Book : NSObject
    @property NSString *name;
    @property NSUInteger pages;
    @property Author *author; //Book 包含 Author 属性
    @end
    @implementation Book
    @end


    安装

    CocoaPods

    1. 在 Podfile 中添加 pod 'YYModel'
    2. 执行 pod install 或 pod update
    3. 导入

    Carthage

    1. 在 Cartfile 中添加 github "ibireme/YYModel"
    2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
    3. 导入

    手动安装

    1. 下载 YYModel 文件夹内的所有内容。
    2. 将 YYModel 内的源文件添加(拖放)到你的工程。
    3. 导入 YYModel.h


    常见问题及demo:点击这里

    源码下载:YYModel.zip



    收起阅读 »

    提建议找bug,领京东卡,环信IM SDK等你来找茬啦~

    环信作为中国即时通讯云的开创者一直致力于为开发者提供简单/易用/稳定/完美的产品和服务。我们深知产品和服务没有极限和尽头,为了给开发者提供超出预期的产品和服务,我们诚邀小伙伴们积极参与和帮助。所谓人人为我,我为人人,让我们一起来找bug,一起提建议,一起来打造...
    继续阅读 »

    环信作为中国即时通讯云的开创者一直致力于为开发者提供简单/易用/稳定/完美的产品和服务。我们深知产品和服务没有极限和尽头,为了给开发者提供超出预期的产品和服务,我们诚邀小伙伴们积极参与和帮助。所谓人人为我,我为人人,让我们一起来找bug,一起提建议,一起来打造世界上最好用的即时通讯云sdk叭!

    活动时间

    4月1日-4月30日


    参与方式

    下载最新版环信IM SDK (https://www.easemob.com/download/im) ,依照开发文档进行模拟操作,找出开发文档或SDK或Demo中的bug或建议,即可参与活动!

    集成成功的截图和产品建议请回复本帖,bug等问题发送邮件至:market@easemob.com

     

    奖励规则

     

    参与方式

    描述及示例

    奖励

    1

    成功集成环信IM SDK!

    在本帖回复成功集成SDK的截图,并进官方红包群。

    如:iOS 开发者:截图环信后台AppKey + 环信SDK初始化代码 (截到KEY 和 编译成功) + 项目的Bundle identifier 

    Android开发者截图环信后台APPKey+项目集成的KEY +项目的applicationId

    其他客户端以此类推。

    50%中奖率抽50元京东卡,不定时群红包

    2

    一般功能bug

    EaseUIKit UI

    及兼容性问题

    例如:某功能没有达到预期效果(iOS设置头像圆角不生效)或响应结果与文档描述不符等情况;

    界面(EaseUIKit)样式错乱,或在不同机型上出现的特异性问题;

    50元京东卡

    3

    重大bug发现

    使用环信SDK时出现异常崩溃 崩溃信息指向sdk需提供异常信息log日志

    SDK某个API调用设置不生效/部分监听回调不执行等

    200元京东卡

    4

    用户体验型建议(含Demo)

    页面提示不友好等用户体验问题,对操作和引导不产生影响,属于使用建议。用户体验型问题(包括Demo应用体验)

    IMGeek定制T恤

    5

    对环信IM SDK提出有效的产品建议

    提出具体的产品建议信息,可以是交互形式,产品新功能等

    IMGeek定制T恤

     

    6

    开发文档修正建议

    包括错误的内容、不合理的文档结构等

    例如:参数错误、文档接口过时或文档接口与SDK不符等

    IMGeek定制T恤

    7

    其他回帖内容

    除以上回帖内容外,其他回帖根据内容酌情奖励

     

     

    活动规则

    1、集成成功的截图和产品建议请回复本帖~!经确认符合活动要求,将私信联系你领奖。

    2、发现bug等问题,请提供demo与复现步骤,将问题发送邮件至 market@easemob.com,邮件内容经官方确认属实,邮件联系你领取奖励;如不是产品bug,会给予回复或解决方法;

    3、如果有相同的bug提交 ,按收到邮件先后顺序奖励较早的提交者;

    4、反馈多条问题时,定制T恤奖励不叠加,京东卡可叠加至200元上限;

    5、本次活动不限客户端;

     

    成功集成SDK截图举例:

     



     


    活动奖励



    官方活动群



    新集成环信SDK的用户请留意私信,冬冬将拉你进抽奖群。

    PS:如果推荐朋友集成环信IM SDK,推荐人与被推荐人都可进群参与抽奖哦!

     

    相关地址及开发文档获取

    开发者帐号注册地址:https://console.easemob.com/user/register

    登录console地址:https://console.easemob.com/user/login

    IM SDK及Demo下载:https://www.easemob.com/download/im

    安卓端开发文档:http://docs-im.easemob.com/im/android/sdk/import

    iOS端开发文档:http://docs-im.easemob.com/im/ios/sdk/prepare

    WEB端开发文档:http://docs-im.easemob.com/im/web/intro/start

    小程序端开发文档:http://docs-im.easemob.com/im/applet/solution

    桌面端开发文档:http://docs-im.easemob.com/im/pc/intro/integration

    Linux端开发文档:http://docs-im.easemob.com/im/linux/integration

    服务端开发文档:http://docs-im.easemob.com/im/server/ready/intro

    常见开发场景说明:http://docs-im.easemob.com/im/other/integrationcases/live-chatroo

     

    *活动最终解释权归环信IMGeek社区所有。

    收起阅读 »

    iOS 超好用的图表库

    AAChartKit前言AAChartKit 项目,是AAInfographics的 Objective-C 语言版本,是在流行的开源前端图表库Highcharts的基础上,封装的面向对象的,一组简单易用,极其精美的图表绘制控件....
    继续阅读 »

    AAChartKit

    前言

    AAChartKit 项目,是AAInfographics的 Objective-C 语言版本,是在流行的开源前端图表库Highcharts的基础上,封装的面向对象的,一组简单易用,极其精美的图表绘制控件.可能是这个星球上 UI 最精致的第三方 iOS 开源图表库了(✟我以无神论者的名义向上帝起誓🖐,我真的没有在说鬼话✟)

    功能特性

    🎂  环境友好,兼容性强. 适配 iOS 8+, 支持iOS iPad OSmacOS, 支持 Objective-C语言, 同时更有 Swift 语言版本 AAInfographics  Java 语言版本 AAChartCore Kotlin 语言版本 AAChartCore-Kotlin 可供使用, 配置导入工程简单易操作. 支持的所有语言版本及连接,参见此列表.

    🚀  功能强大,类型多样 -. 支持柱状图 条形图 折线图 曲线图 折线填充图 曲线填充图雷达图极地图扇形图气泡图散点图区域范围图柱形范围图面积范围图面积范围均线图直方折线图直方折线填充图箱线图瀑布图热力图桑基图金字塔图漏斗图、等二十几种类型的图形,不可谓之不多.

    📝  现代化声明式语法. 与过往的命令式编程技巧不同, 在 AAChartKit 中绘制任意一款自定义图表, 你完全无需关心挠人的内在实现细节. 描述你所要得到的, 你便得到你所描述的.

    🔬  细致入微的用户自定义功能. 基础的主标题副标题X 轴Y 轴自不必谈, 从纵横的交互准星线、跟手的浮动提示框, 到切割数值的值域分割线值域分割颜色带, 再到细小的线条类型,标记点样式, 各种细微的图形子组件, 应有尽有. 以至于不论是极简、抽象的小清新风格, 还是纷繁复杂的严肃商业派头, 均可完美驾驭.

    🎮  交互式图形动画 . 有着清晰和充满细节的用户交互方式, 与此同时, 图形渲染动画效果细腻精致, 流畅优美. 有三十多种以上渲染动画效果可供选择, 用户可自由设置渲染图形时的动画时间和动画类型, 关于图形渲染动画类型,具体参见 AAChartKit 动画类型.

    🦋  极简主义 . AAChartView + AAChartModel = Chart,在 AAChartKit 图表框架当中,遵循这样一个极简主义公式:图表视图控件 + 图表模型 = 你想要的图表. 同另一款强大而又精美的图表库AAInfographics完全一致.

      链式编程语法 . 支持类 Masonry 链式编程语法, 一行代码即可配置完成 AAChartModel模型对象实例.

    🎈  简洁清晰,轻便易用 . 最少仅仅需要 五行代码 即可完成整个图表的绘制工作(使用链式编程语法配置 AAChartModel 实例对象时, 无论你写多少行代码, 理论上只能算作是一行). 🤪🤪🤪

    🖱  交互事件回调 支持图表的用户点击事件及单指滑动事件, 可在此基础上实现双表联动乃至多表联动,以及其他更多更复杂的自定义用户交互效果.

    👌  支持手势缩放 . 支持各个方向的图表手势缩放和拖动阅览, 手势缩放类型具体参见 AAChartKit 手势缩放类型, 默认禁用手势缩放功能

    效果图


    CocoaPods 安装 (推荐)

    1. 在 Podfile 中添加以下内容
    pod 'AAChartKit', :git => 'https://github.com/AAChartModel/AAChartKit.git'
    1. 执行  pod install 或  pod update

    手动安装
    1. 将项目Demo中的文件夹AAChartKitLib拖入到所需项目中.
    2. 在你的项目的 .pch 全局宏定义文件中添加
    #import "AAGlobalMacro.h"

    使用

    1. 在你的ViewController视图控制器文件中添加
    #import "AAChartKit.h"
    1. 创建视图AAChartView
    CGFloat chartViewWidth  = self.view.frame.size.width;
    CGFloat chartViewHeight = self.view.frame.size.height - 250;
    _aaChartView = [[AAChartView alloc]init];
    _aaChartView.frame = CGRectMake(0, 60, chartViewWidth, chartViewHeight);
    ////禁用 AAChartView 滚动效果(默认不禁用)
    //self.aaChartView.scrollEnabled = NO;
    [self.view addSubview:_aaChartView];
    1. 配置视图模型AAChartModel
    AAChartModel *aaChartModel= AAObject(AAChartModel)
    .chartTypeSet(AAChartTypeArea)//设置图表的类型(这里以设置的为折线面积图为例)
    .titleSet(@"编程语言热度")//设置图表标题
    .subtitleSet(
    @"虚拟数据")//设置图表副标题
    .categoriesSet(@[@"Java",@"Swift",@"Python",@"Ruby", @"PHP",@"Go",@"C",@"C#",@"C++"])//图表横轴的内容
    .yAxisTitleSet(
    @"摄氏度")//设置图表 y 轴的单位
    .seriesSet(@[
    AAObject(AASeriesElement)
    .nameSet(@"2017")
    .
    dataSet(@[@7.0, @6.9, @9.5, @14.5, @18.2, @21.5, @25.2, @26.5, @23.3, @18.3, @13.9, @9.6]),
    AAObject(AASeriesElement)
    .
    nameSet(@"2018")
    .dataSet(@[@0.2, @0.8, @5.7, @11.3, @17.0, @22.0, @24.8, @24.1, @20.1, @14.1, @8.6, @2.5]),
    AAObject(AASeriesElement)
    .nameSet(@"2019")
    .
    dataSet(@[@0.9, @0.6, @3.5, @8.4, @13.5, @17.0, @18.6, @17.9, @14.3, @9.0, @3.9, @1.0]),
    AAObject(AASeriesElement)
    .
    nameSet(@"2020")
    .dataSet(@[@3.9, @4.2, @5.7, @8.5, @11.9, @15.2, @17.0, @16.6, @14.2, @10.3, @6.6, @4.8]),
    ])
    ;
    1. 绘制图形(创建 AAChartView 实例对象后,首次绘制图形调用此方法)
    /*图表视图对象调用图表模型对象,绘制最终图形*/
    [_aaChartView aa_drawChartWithChartModel:aaChartModel];



    当前已支持的图表类型有十种以上,说明如下

    typedef NSString *AAChartType;

    AACHARTKIT_EXTERN AAChartType const AAChartTypeColumn; //柱形图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeBar; //条形图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeArea; //折线区域填充图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeAreaspline; //曲线区域填充图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeLine; //折线图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeSpline; //曲线图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeScatter; //散点图
    AACHARTKIT_EXTERN AAChartType const AAChartTypePie; //扇形图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeBubble; //气泡图
    AACHARTKIT_EXTERN AAChartType const AAChartTypePyramid; //金字塔图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeFunnel; //漏斗图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeColumnrange; //柱形范围图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeArearange; //区域折线范围图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeAreasplinerange; //区域曲线范围图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeBoxplot; //箱线图
    AACHARTKIT_EXTERN AAChartType const AAChartTypeWaterfall; //瀑布图
    AACHARTKIT_EXTERN AAChartType const AAChartTypePolygon; //多边形图

    当前已支持的图表手势缩放类型共有三种,说明如下

    typedef NSString *AAChartZoomType;

    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeNone; //禁用手势缩放功能(默认禁用手势缩放)
    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeX; //支持图表 X轴横向缩放
    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeY; //支持图表 Y轴纵向缩放
    AACHARTKIT_EXTERN AAChartZoomType const AAChartZoomTypeXY; //支持图表等比例缩放

    AAChartModel 属性配置列表

    AAPropStatementAndPropSetFuncStatement(copy,   AAChartModel, NSString *, title) //标题文本内容
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, titleFontSize) //标题字体尺寸大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, titleFontColor) //标题字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, titleFontWeight) //标题字体粗细

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitle) //副标题文本内容
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, subtitleFontSize) //副标题字体尺寸大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitleFontColor) //副标题字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, subtitleFontWeight) //副标题字体粗细

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, backgroundColor) //图表背景色(必须为十六进制的颜色色值如红色"#FF0000")
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray <NSString *>*, colorsTheme) //图表主题颜色数组
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray <NSString *>*, categories) //x轴坐标每个点对应的名称(注意:这个不是用来设置 X 轴的值,仅仅是用于设置 X 轴文字内容的而已)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, series) //图表的数据列内容

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartSubtitleAlignType, subtitleAlign) //图表副标题文本水平对齐方式。可选的值有 “left”,”center“和“right”。 默认是:center.
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartType, chartType) //图表类型
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartStackingType, stacking) //堆积样式
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartSymbolType, markerSymbol) //折线曲线连接点的类型:"circle", "square", "diamond", "triangle","triangle-down",默认是"circle"
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AAChartSymbolStyleType, markerSymbolStyle)
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartZoomType, zoomType) //缩放类型 AAChartZoomTypeX 表示可沿着 x 轴进行手势缩放
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AAChartAnimation, animationType) //设置图表的渲染动画类型
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, animationDuration) //设置图表的渲染动画时长(动画单位为毫秒)

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, inverted) //x 轴是否垂直,默认为否
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, gradientColorsThemeEnabled) //是否将常规主题颜色数组 colorsTheme 自动转换为半透明渐变效果的颜色数组(设置后就不用自己再手动去写渐变色字典,相当于是设置渐变色的一个快捷方式,当然了,如果需要细致地自定义渐变色效果,还是需要自己手动配置渐变颜色字典内容,具体方法参见图表示例中的`颜色渐变条形图`示例代码),默认为否
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, polar) //是否极化图形(变为雷达图),默认为否

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, dataLabelEnabled) //是否显示数据,默认为否
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, dataLabelFontColor) //Datalabel font color
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, dataLabelFontSize) //Datalabel font size
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, dataLabelFontWeight) //Datalabel font weight


    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisVisible) //x 轴是否可见(默认可见)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisReversed) // x 轴翻转,默认为否

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, xAxisLabelsEnabled) //x 轴是否显示文字
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisLabelsFontSize) //x 轴文字字体大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, xAxisLabelsFontColor) //x 轴文字字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartFontWeightType, xAxisLabelsFontWeight) //x 轴文字字体粗细

    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisGridLineWidth) //x 轴网格线的宽度
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisTickInterval) //x轴刻度点间隔数(设置每隔几个点显示一个 X轴的内容)

    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, xAxisCrosshairWidth) //设置 x 轴准星线的宽度
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, xAxisCrosshairColor) //设置 x 轴准星线的颜色
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AALineDashSyleType, xAxisCrosshairDashStyleType) //设置 x 轴准星线的线条样式类型


    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisVisible) //y 轴是否可见(默认可见)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisReversed) //y 轴翻转,默认为否

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisLabelsEnabled) //y 轴是否显示文字
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisLabelsFontSize) //y 轴文字字体大小
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisLabelsFontColor) //y 轴文字字体颜色
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, AAChartFontWeightType , yAxisLabelsFontWeight) //y 轴文字字体粗细

    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisTitle) //y 轴标题
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisLineWidth) //y y-axis line width
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisGridLineWidth) //y轴网格线的宽度
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, yAxisAllowDecimals) //是否允许 y 轴显示小数
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, yAxisPlotLines) //y 轴基线的配置
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisMax) //y 轴最大值
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisMin) //y 轴最小值(设置为0就不会有负数)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisTickInterval)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSArray *, yAxisTickPositions) //自定义 y 轴坐标(如:[@(0), @(25), @(50), @(75) , (100)])

    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, yAxisCrosshairWidth) //设置 y 轴准星线的宽度
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, yAxisCrosshairColor) //设置 y 轴准星线的颜色
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, AALineDashSyleType, yAxisCrosshairDashStyleType) //设置 y 轴准星线的线条样式类型


    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, tooltipEnabled) //是否显示浮动提示框(默认显示)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, tooltipShared)//是否多组数据共享一个浮动提示框
    AAPropStatementAndPropSetFuncStatement(copy, AAChartModel, NSString *, tooltipValueSuffix) //浮动提示框单位后缀

    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, connectNulls) //设置折线是否断点重连(是否连接空值点)
    AAPropStatementAndPropSetFuncStatement(assign, AAChartModel, BOOL, legendEnabled) //是否显示图例 lengend(图表底部可点按的圆点和文字)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, borderRadius) //柱状图长条图头部圆角半径(可用于设置头部的形状,仅对条形图,柱状图有效)
    AAPropStatementAndPropSetFuncStatement(strong, AAChartModel, NSNumber *, markerRadius) //折线连接点的半径长度

    源码下载:AAChartKit-demo.zip

    常见问题及详细说明:点击这里



    收起阅读 »

    iOS 相册选择器推荐

    HXPhotoPicker效果预览特性 - Features 查看、选择GIF图片 照片、视频可同时多选/原图 3DTouch预览照片 长按拖动改变顺序 自定义相机拍照、录制视频 自定义转场动画&nb...
    继续阅读 »

    HXPhotoPicker

    效果预览



    特性 - Features

    •  查看、选择GIF图片
    •  照片、视频可同时多选/原图
    •  3DTouch预览照片
    •  长按拖动改变顺序
    •  自定义相机拍照、录制视频
    •  自定义转场动画
    •  查看、选择LivePhoto iOS9.1以上才有用
    •  浏览网络图片、网络视频
    •  仿微信编辑图片功能
    •  自定义裁剪视频时长
    •  传入本地图片、视频
    •  在线下载iCloud上的资源
    •  两种相册展现方式(列表、弹窗)
    •  支持Cell上添加
    •  支持草稿功能
    •  同一界面多个不同选择器
    •  支持暗黑模式
    •  支持横向布局
    •  支持Xib和Masonry布局
    •  支持自定义item的大小
    •  支持滑动手势选择

    安装 - Installation

    CocoaPods
    # 将以下内容添加到您的Podfile中:
    # 不使用网络图片功能
    pod 'HXPhotoPicker', '~> 3.1.9'

    # 使用SDWebImage加载网络图片
    pod 'HXPhotoPicker/SDWebImage', '~> 3.1.9'

    # 使用YYWebImage加载网络图片
    pod 'HXPhotoPicker/YYWebImage', '~> 3.1.9'

    # 搜索不到库或最新版时请执行
    pod repo update rm ~/Library/Caches/CocoaPods/search_index.json
    Carthage
    # 将以下内容添加到您的Cartfile中:
    github "SilenceLove/HXPhotoPicker"
    手动导入
    手动导入:将项目中的“HXPhotoPicker”文件夹拖入项目中
    使用前导入头文件 "HXPhotoPicker.h"

    要求 - Requirements

    • iOS8及以上系统可使用. ARC环境. - iOS 8 or later. Requires ARC
    • 访问相册和相机需要配置四个info.plist文件
    • Privacy - Photo Library Usage Description 和 Privacy - Camera Usage Description 以及 Privacy - Microphone Usage Description
    • Privacy - Location When In Use Usage Description 使用相机拍照时会获取位置信息
    • 相机拍照功能请使用真机调试

    应用示例 - Examples

    跳转相册选择照片

    // 懒加载 照片管理类
    - (HXPhotoManager *)manager {
    if (!_manager) {
    _manager = [[HXPhotoManager alloc] initWithType:HXPhotoManagerSelectedTypePhotoAndVideo];
    }
    return _manager;
    }

    // 方法一:
    HXWeakSelf
    [self hx_presentSelectPhotoControllerWithManager:self.manager didDone:^(NSArray *allList, NSArray *photoList, NSArray *videoList, BOOL isOriginal, UIViewController *viewController, HXPhotoManager *manager) {
    weakSelf.total.text = [NSString stringWithFormat:@"总数量:%ld ( 照片:%ld 视频:%ld )",allList.count, photoList.count, videoList.count];
    weakSelf.original.text = isOriginal ? @"YES" : @"NO";
    NSSLog(@"block - all - %@",allList);
    NSSLog(@"block - photo - %@",photoList);
    NSSLog(@"block - video - %@",videoList);
    } cancel:^(UIViewController *viewController, HXPhotoManager *manager) {
    NSSLog(@"block - 取消了");
    }];

    // 方法二:
    // 照片选择控制器
    HXCustomNavigationController *nav = [[HXCustomNavigationController alloc] initWithManager:self.manager delegate:self];
    [self presentViewController:nav animated:YES completion:nil];

    // 通过 HXCustomNavigationControllerDelegate 代理返回选择的图片以及视频
    /**
    点击完成按钮

    @param photoNavigationViewController self
    @param allList 已选的所有列表(包含照片、视频)
    @param photoList 已选的照片列表
    @param videoList 已选的视频列表
    @param original 是否原图
    */
    - (void)photoNavigationViewController:(HXCustomNavigationController *)photoNavigationViewController didDoneAllList:(NSArray *)allList photos:(NSArray *)photoList videos:(NSArray *)videoList original:(BOOL)original;

    /**
    点击取消

    @param photoNavigationViewController self
    */
    - (void)photoNavigationViewControllerDidCancel:(HXCustomNavigationController *)photoNavigationViewController;

    单独使用HXPhotoPreviewViewController预览图片

    HXCustomAssetModel *assetModel1 = [HXCustomAssetModel assetWithLocaImageName:@"1" selected:YES];
    // selected 为NO 的会过滤掉
    HXCustomAssetModel *assetModel2 = [HXCustomAssetModel assetWithLocaImageName:@"2" selected:NO];
    HXCustomAssetModel *assetModel3 = [HXCustomAssetModel assetWithNetworkImageURL:[NSURL URLWithString:@"http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/1466408576222.jpg"] selected:YES];
    // selected 为NO 的会过滤掉
    HXCustomAssetModel *assetModel4 = [HXCustomAssetModel assetWithNetworkImageURL:[NSURL URLWithString:@"http://tsnrhapp.oss-cn-hangzhou.aliyuncs.com/0034821a-6815-4d64-b0f2-09103d62630d.jpg"] selected:NO];
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"QQ空间视频_20180301091047" withExtension:@"mp4"];
    HXCustomAssetModel *assetModel5 = [HXCustomAssetModel assetWithLocalVideoURL:url selected:YES];

    HXPhotoManager *photoManager = [HXPhotoManager managerWithType:HXPhotoManagerSelectedTypePhotoAndVideo];
    photoManager.configuration.saveSystemAblum = YES;
    photoManager.configuration.photoMaxNum = 0;
    photoManager.configuration.videoMaxNum = 0;
    photoManager.configuration.maxNum = 10;
    photoManager.configuration.selectTogether = YES;
    photoManager.configuration.photoCanEdit = NO;
    photoManager.configuration.videoCanEdit = NO;

    HXWeakSelf
    // 长按事件
    photoManager.configuration.previewRespondsToLongPress = ^(UILongPressGestureRecognizer *longPress,
    HXPhotoModel *photoModel,
    HXPhotoManager *manager,
    HXPhotoPreviewViewController *previewViewController) {
    hx_showAlert(previewViewController, @"提示", @"长按事件", @"确定", nil, nil, nil);
    };
    // 跳转预览界面时动画起始的view
    photoManager.configuration.customPreviewFromView = ^UIView *(NSInteger currentIndex) {
    HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
    return viewCell;
    };
    // 跳转预览界面时展现动画的image
    photoManager.configuration.customPreviewFromImage = ^UIImage *(NSInteger currentIndex) {
    HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
    return viewCell.imageView.image;
    };
    // 退出预览界面时终点view
    photoManager.configuration.customPreviewToView = ^UIView *(NSInteger currentIndex) {
    HXPhotoSubViewCell *viewCell = [weakSelf.photoView collectionViewCellWithIndex:currentIndex];
    return viewCell;
    };
    [photoManager addCustomAssetModel:@[assetModel1, assetModel2, assetModel3, assetModel4, assetModel5]];

    [self hx_presentPreviewPhotoControllerWithManager:photoManager
    previewStyle:HXPhotoViewPreViewShowStyleDark
    currentIndex:0
    photoView:nil];


    UIViewController+HXExtension.h
    /// 跳转预览照片界面
    /// @param manager 照片管理者
    /// @param previewStyle 预览样式
    /// @param currentIndex 当前预览的下标
    /// @param photoView 照片展示视图 - 没有就不传
    - (void)hx_presentPreviewPhotoControllerWithManager:(HXPhotoManager *)manager
    previewStyle:(HXPhotoViewPreViewShowStyle)previewStyle
    currentIndex:(NSUInteger)currentIndex
    photoView:(HXPhotoView * _Nullable)photoView;


    使用如何保存草稿

    通过 HXPhotoManager 对象进行存储
    /// 获取保存在本地文件的模型数组
    - (NSArray *)getLocalModelsInFile;

    /// 将模型数组保存到本地文件
    - (BOOL)saveLocalModelsToFile;

    /// 将保存在本地文件的模型数组删除
    - (BOOL)deleteLocalModelsInFile;

    /// 将本地获取的模型数组添加到manager的数据中
    /// @param models 在本地获取的模型数组
    - (void)addLocalModels:(NSArray *)models;

    /// 将本地获取的模型数组添加到manager的数据中
    - (void)addLocalModels;


    demo:HXPhotoPicker.zip

    常见问题及源码地址:点击这里


    收起阅读 »

    iOS 网络图片加载库

    SDWebImage  一款超级好用的网络图片加载库集成方式pod 'SDWebImage', '~> 5.0'使用方式#import [imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址...
    继续阅读 »

    SDWebImage  一款超级好用的网络图片加载库

    集成方式

    pod 'SDWebImage', '~> 5.0'

    使用方式

    #import <SDWebImage/SDWebImage.h>
    [imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址"]
    placeholderImage:[UIImage imageNamed:@"占位图名字"]];

        加载gif

    SDAnimatedImageView *imageView = [SDAnimatedImageView new];
    SDAnimatedImage *animatedImage = [SDAnimatedImage imageNamed:@"image.gif"];
    imageView.image = animatedImage;

       使用Blocks,采用这个方案可以在网络图片加载过程中得知图片的下载进度和图片加载成功与否

    [imageView sd_setImageWithURL:[NSURL URLWithString:@"图片地址"] placeholderImage:[UIImage imageNamed:@"占位图"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { //... completion code here ... }];

       取图片的缓存大小
    [SDImageCache sharedImageCache] getSize];
       清理内存,磁盘缓存
    [[SDImageCache sharedImageCache] clearMemory];


    常见问题及demo地址:点击这里


    收起阅读 »

    iOS 提示框

    推荐一个好用的iOS 提示框库MBProgressHUD集成方式pod 'MBProgressHUD', '~> 1.2.0'或者直接将附件拖入项目内导入#import "MBProgressHUD.h" 效果图:在这里顺便分享一下使用小技巧我们可以将hud定...
    继续阅读 »

    推荐一个好用的iOS 提示框库

    MBProgressHUD

    集成方式

    pod 'MBProgressHUD', '~> 1.2.0'

    或者直接将附件拖入项目内

    导入#import "MBProgressHUD.h"


    效果图:





    在这里顺便分享一下使用小技巧

    我们可以将hud定义成宏 .

    #pragma mark - hud 提示

    /**

     默认请求开始的hud

     */

    #define HudShow MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.label.text = NSLocalizedString(@"请求中...", @"HUD loading title");


    /**

     自定义title的请求开始hud

     */

    #define HudShowStr(str) MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.label.text = NSLocalizedString(str, @"HUD loading title");


    /**

     移除hud

     */

    #define HudHidden         [hud hideAnimated:YES];


    /**

     自定义title 的提示hud

     */ 

    #define HudMessageStr(str) hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(str, @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\


    /**

     提示的hud .title来自json

     */

    #define HudMessage hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(@"根据需求传值例如取服务器json的某个值", @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\



    /**

     请求错误时的hud

     */

    #define HudError   hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(@"网络差", @"HUD message title"); \

    [hud hideAnimated:YES afterDelay:1.f];




    /**

     alert效果的hud

     */

    #define alertHudShow(str) MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(str, @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\





    /**

     请求时错误才提示的hud

     */

    #define RequestErrorHud MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];\

    hud.mode = MBProgressHUDModeText;\

    hud.label.text = NSLocalizedString(@"网络差", @"HUD message title"); [hud hideAnimated:YES afterDelay:1];\


    源码下载: Hud.zip

    常见问题及demo:点击这里

    收起阅读 »

    ImmersionBar -- android 4.4以上沉浸式实现

    ImmersionBar -- android 4.4以上沉浸式实现使用android studio// 基础依赖包,必须要依赖 implementation 'com.gyf.immersionbar:immersionbar:3.0.0' // fragm...
    继续阅读 »

    ImmersionBar -- android 4.4以上沉浸式实现

    使用

    android studio

    // 基础依赖包,必须要依赖
    implementation 'com.gyf.immersionbar:immersionbar:3.0.0'
    // fragment快速实现(可选)
    implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0'
    // kotlin扩展(可选)
    implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0'

    关于使用AndroidX支持库

    • 如果你的项目中使用了AndroidX支持库,请在你的gradle.properties加入如下配置,如果已经配置了,请忽略
         android.useAndroidX=true
      android.enableJetifier=true

    关于全面屏与刘海

    关于全面屏

    在manifest加入如下配置,四选其一,或者都写

    ① 在manifest的Application节点下加入

       <meta-data 
    android:name="android.max_aspect"
    android:value="2.4" />

    ② 在manifest的Application节点中加入

       android:resizeableActivity="true"

    ③ 在manifest的Application节点中加入

       android:maxAspectRatio="2.4"

    ④ 升级targetSdkVersion为25以上版本

    关于刘海屏

    在manifest的Application节点下加入,vivo和oppo没有找到相关配置信息

       
    <meta-data
    android:name="android.notch_support"
    android:value="true"/>

    <meta-data
    android:name="notch.config"
    android:value="portrait|landscape" />


    Api详解

    • 基础用法

      ImmersionBar.with(this).init();
    • 高级用法(每个参数的意义)

       ImmersionBar.with(this)
      .transparentStatusBar() //透明状态栏,不写默认透明色
      .transparentNavigationBar() //透明导航栏,不写默认黑色(设置此方法,fullScreen()方法自动为true)
      .transparentBar() //透明状态栏和导航栏,不写默认状态栏为透明色,导航栏为黑色(设置此方法,fullScreen()方法自动为true)
      .statusBarColor(R.color.colorPrimary) //状态栏颜色,不写默认透明色
      .navigationBarColor(R.color.colorPrimary) //导航栏颜色,不写默认黑色
      .barColor(R.color.colorPrimary) //同时自定义状态栏和导航栏颜色,不写默认状态栏为透明色,导航栏为黑色
      .statusBarAlpha(0.3f) //状态栏透明度,不写默认0.0f
      .navigationBarAlpha(0.4f) //导航栏透明度,不写默认0.0F
      .barAlpha(0.3f) //状态栏和导航栏透明度,不写默认0.0f
      .statusBarDarkFont(true) //状态栏字体是深色,不写默认为亮色
      .navigationBarDarkIcon(true) //导航栏图标是深色,不写默认为亮色
      .autoDarkModeEnable(true) //自动状态栏字体和导航栏图标变色,必须指定状态栏颜色和导航栏颜色才可以自动变色哦
      .autoStatusBarDarkModeEnable(true,0.2f) //自动状态栏字体变色,必须指定状态栏颜色才可以自动变色哦
      .autoNavigationBarDarkModeEnable(true,0.2f) //自动导航栏图标变色,必须指定导航栏颜色才可以自动变色哦
      .flymeOSStatusBarFontColor(R.color.btn3) //修改flyme OS状态栏字体颜色
      .fullScreen(true) //有导航栏的情况下,activity全屏显示,也就是activity最下面被导航栏覆盖,不写默认非全屏
      .hideBar(BarHide.FLAG_HIDE_BAR) //隐藏状态栏或导航栏或两者,不写默认不隐藏
      .addViewSupportTransformColor(toolbar) //设置支持view变色,可以添加多个view,不指定颜色,默认和状态栏同色,还有两个重载方法
      .titleBar(view) //解决状态栏和布局重叠问题,任选其一
      .titleBarMarginTop(view) //解决状态栏和布局重叠问题,任选其一
      .statusBarView(view) //解决状态栏和布局重叠问题,任选其一
      .fitsSystemWindows(true) //解决状态栏和布局重叠问题,任选其一,默认为false,当为true时一定要指定statusBarColor(),不然状态栏为透明色,还有一些重载方法
      .supportActionBar(true) //支持ActionBar使用
      .statusBarColorTransform(R.color.orange) //状态栏变色后的颜色
      .navigationBarColorTransform(R.color.orange) //导航栏变色后的颜色
      .barColorTransform(R.color.orange) //状态栏和导航栏变色后的颜色
      .removeSupportView(toolbar) //移除指定view支持
      .removeSupportAllView() //移除全部view支持
      .navigationBarEnable(true) //是否可以修改导航栏颜色,默认为true
      .navigationBarWithKitkatEnable(true) //是否可以修改安卓4.4和emui3.x手机导航栏颜色,默认为true
      .navigationBarWithEMUI3Enable(true) //是否可以修改emui3.x手机导航栏颜色,默认为true
      .keyboardEnable(true) //解决软键盘与底部输入框冲突问题,默认为false,还有一个重载方法,可以指定软键盘mode
      .keyboardMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) //单独指定软键盘模式
      .setOnKeyboardListener(new OnKeyboardListener() { //软键盘监听回调,keyboardEnable为true才会回调此方法
      @Override
      public void onKeyboardChange(boolean isPopup, int keyboardHeight) {
      LogUtils.e(isPopup); //isPopup为true,软键盘弹出,为false,软键盘关闭
      }
      })
      .setOnNavigationBarListener(onNavigationBarListener) //导航栏显示隐藏监听,目前只支持华为和小米手机
      .setOnBarListener(OnBarListener) //第一次调用和横竖屏切换都会触发,可以用来做刘海屏遮挡布局控件的问题
      .addTag("tag") //给以上设置的参数打标记
      .getTag("tag") //根据tag获得沉浸式参数
      .reset() //重置所以沉浸式参数
      .init(); //必须调用方可应用以上所配置的参数

    在Activity中实现沉浸式

    • java用法

       ImmersionBar.with(this).init();
    • kotlin用法

       immersionBar {
      statusBarColor(R.color.colorPrimary)
      navigationBarColor(R.color.colorPrimary)
      }

    在Fragment中实现沉浸式

    在Fragment使用ImmersionBar


    在Dialog中实现沉浸式,具体实现参考demo

    • ①结合dialogFragment使用,可以参考demo中的BaseDialogFragment这个类

          ImmersionBar.with(this).init();
    • ②其他dialog,关闭dialog的时候必须调用销毁方法

          ImmersionBar.with(this, dialog).init();

      销毁方法:

      java中

          ImmersionBar.destroy(this, dialog);

      kotlin中

          destroyImmersionBar(dialog)


    在PopupWindow中实现沉浸式,具体实现参考demo

    重点是调用以下方法,但是此方法会导致有导航栏的手机底部布局会被导航栏覆盖,还有底部输入框无法根据软键盘弹出而弹出,具体适配请参考demo。

        popupWindow.setClippingEnabled(false);

    状态栏与布局顶部重叠解决方案,六种方案根据不同需求任选其一

    • ① 使用dimen自定义状态栏高度,不建议使用,因为设备状态栏高度并不是固定的

      在values-v19/dimens.xml文件下

          <dimen name="status_bar_height">25dpdimen>

      在values/dimens.xml文件下

          <dimen name="status_bar_height">0dpdimen>

      然后在布局界面添加view标签,高度指定为status_bar_height

         <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:background="@color/darker_gray"
      android:orientation="vertical">

      <View
      android:layout_width="match_parent"
      android:layout_height="@dimen/status_bar_height"
      android:background="@color/colorPrimary" />

      <android.support.v7.widget.Toolbar
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="@color/colorPrimary"
      app:title="方法一"
      app:titleTextColor="@android:color/white" />
      LinearLayout>
    • ② 使用系统的fitsSystemWindows属性,使用该属性不会导致输入框与软键盘冲突问题,不要再Fragment使用该属性,只适合纯色状态栏

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

      然后使用ImmersionBar时候必须指定状态栏颜色

          ImmersionBar.with(this)
      .statusBarColor(R.color.colorPrimary)
      .init();
      • 注意:ImmersionBar一定要在设置完布局以后使用,
    • ③ 使用ImmersionBar的fitsSystemWindows(boolean fits)方法,只适合纯色状态栏

          ImmersionBar.with(this)
      .fitsSystemWindows(true) //使用该属性,必须指定状态栏颜色
      .statusBarColor(R.color.colorPrimary)
      .init();
    • ④ 使用ImmersionBar的statusBarView(View view)方法,可以用来适配渐变色状态栏、侧滑返回

      在标题栏的上方增加View标签,高度指定为0dp

          <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:background="@color/darker_gray"
      android:orientation="vertical">

      <View
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:background="@color/colorPrimary" />

      <android.support.v7.widget.Toolbar
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="@color/colorPrimary"
      app:title="方法四"
      app:titleTextColor="@android:color/white" />
      LinearLayout>

      然后使用ImmersionBar的statusBarView方法,指定view就可以啦

           ImmersionBar.with(this)
      .statusBarView(view)
      .init();
      //或者
      //ImmersionBar.setStatusBarView(this,view);
    • ⑤ 使用ImmersionBar的titleBar(View view)方法,原理是设置paddingTop,可以用来适配渐变色状态栏、侧滑返回

               ImmersionBar.with(this)
      .titleBar(view) //可以为任意view,如果是自定义xml实现标题栏的话,标题栏根节点不能为RelativeLayout或者ConstraintLayout,以及其子类
      .init();
      //或者
      //ImmersionBar.setTitleBar(this, view);
    • ⑥ 使用ImmersionBar的titleBarMarginTop(View view)方法,原理是设置marginTop,只适合纯色状态栏

               ImmersionBar.with(this)
      .titleBarMarginTop(view) //可以为任意view
      .statusBarColor(R.color.colorPrimary) //指定状态栏颜色,根据情况是否设置
      .init();
      //或者使用静态方法设置
      //ImmersionBar.setTitleBarMarginTop(this,view);

    解决EditText和软键盘的问题

    • 第一种方案
          ImmersionBar.with(this)
      .keyboardEnable(true) //解决软键盘与底部输入框冲突问题
      // .keyboardEnable(true, WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
      // | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) //软键盘自动弹出
      .init();
    • 第二种方案 不使用keyboardEnable方法,只需要在布局的根节点(最外层节点)加上android:fitsSystemWindows="true"属性即可,只适合纯色状态栏

    当白色背景状态栏遇到不能改变状态栏字体为深色的设备时,解决方案

          ImmersionBar.with(this)
    .statusBarDarkFont(true, 0.2f) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度
    .init();


    状态栏和导航栏其它方法

    • public static boolean hasNavigationBar(Activity activity)

      判断是否存在导航栏

    • public static int getNavigationBarHeight(Activity activity)

      获得导航栏的高度

    • public static int getNavigationBarWidth(Activity activity)

      获得导航栏的宽度

    • public static boolean isNavigationAtBottom(Activity activity)

      判断导航栏是否在底部

    • public static int getStatusBarHeight(Activity activity)

      获得状态栏的高度

    • public static int getActionBarHeight(Activity activity)

      获得ActionBar的高度

    • public static boolean hasNotchScreen(Activity activity)

      是否是刘海屏

    • public static boolean getNotchHeight(Activity activity)

      获得刘海屏高度

    • public static boolean isSupportStatusBarDarkFont()

      判断当前设备支不支持状态栏字体设置为黑色

    • public static boolean isSupportNavigationIconDark()

      判断当前设备支不支持导航栏图标设置为黑色

    • public static void hideStatusBar(Window window)

      隐藏状态栏

    混淆规则(proguard-rules.pro)

     -keep class com.gyf.immersionbar.* {*;} 
    -dontwarn com.gyf.immersionbar.**



    代码下载 :ImmersionBar-master.zip

    原文链接:https://github.com/gyf-dev/ImmersionBar


    收起阅读 »

    material风格-DialogUtil

    DialogUtilmaterial风格(v7支持包中的),ios风格,自动获取顶层activity,可在任意界面弹出,可在任意线程弹出注意点在activity已经resume后再调用,不要在onstart里用,否则可能会不显示. 如果非要在onstart里,...
    继续阅读 »

    DialogUtil

    material风格(v7支持包中的),ios风格,自动获取顶层activity,可在任意界面弹出,可在任意线程弹出

    注意点

    • 在activity已经resume后再调用,不要在onstart里用,否则可能会不显示. 
    • 如果非要在onstart里,就记得调用setActivity()
    • 如果有的国产机不显示,就调用setActivity()
    • 不要滥用loadingdialog:

    注意使用的场景:

     第一此进入页面,用layout内部的loadingview,有很多statelayout框架,
    再次刷新,用刷新头显示刷新状态
    局部刷新或点击某按钮访问网络,用loading dialog,不影响页面本身状态,类似web中的ajax请求.

    特性

    • **自动获取顶层activity,**无需传入activity也可弹出dialog.如果传入,则指定在此activity弹出.
    • 安全,任意线程均可调用.
    • 类型丰富,包括常用的ios风格dialog和material design风格的dialog,且按钮和文字样式可便捷地修改
    • 自定义view:可以传入自定义的view,定义好事件,本工具负责安全地显示
    • 也可以保留iso样式或material 样式的底部按钮和上方title(可隐藏),中间的view可以完全自定义
    • 考虑了显示内容超多时的滑动和与屏幕的间隙.
    • 也可以设置宽高百分比来自定义宽高
    • 可以关闭默认的阴影背景,从而能使用xml中自定义的背景(弹出自定义view的dialog时常用)
    • 支持国际化
    • 智能弹出和隐藏软键盘.自定义view的dialog只要设置setNeedSoftKeyboard为true,即可自动处理软键盘的弹出和隐藏
    • ios样式和material 样式的均可以在三种状态下显示: 普通dialog,TYPE_TOAST,作为activity.(原生ProgressDialog和Design包下的BottomSheetDialog除外,其在TYPE_TOAST或activity显示有异样)
    • 支持带x的广告样式的动画

    useage

    gradle

    Step 1. Add the JitPack repository to your build file

    Add it in your root build.gradle at the end of repositories:

    	allprojects {
    repositories {
    ...
    maven { url "https://jitpack.io" }
    }
    }

    Step 2. Add the dependency

    	dependencies {
           compile ('com.github.hss01248:DialogUtil:lastest release'){
    exclude group: 'com.android.support'
           }
            compile 'com.android.support:appcompat-v7:26.1.0'
    compile 'com.android.support:recyclerview-v7:26.1.0'
    compile 'com.android.support:design:26.1.0'
    //将26.1.0: 改为自己项目中一致的版本
    }

    lastest release: https://github.com/hss01248/DialogUtil/releases

    初始化

    //在Application的oncreate方法里:
    传入context
    StyledDialog.init(this);

    activity生命周期callback中拿到顶层activity引用:
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    ActivityStackManager.getInstance().addActivity(activity);
    }

    @Override
    public void onActivityStarted(Activity activity) {

    }

    @Override
    public void onActivityResumed(Activity activity) {
    }

    @Override
    public void onActivityPaused(Activity activity) {

    }

    @Override
    public void onActivityStopped(Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    ActivityStackManager.getInstance().removeActivity(activity);
    }
    });

    示例代码(MainActivity里)

            //使用默认样式时,无须.setxxx:
    StyledDialog.buildLoading().show();

    //自定义部分样式时:
    StyledDialog.buildMdAlert("title", msg, new MyDialogListener() {
    @Override
    public void onFirst() {
    showToast("onFirst");
    }

    @Override
    public void onSecond() {
    showToast("onSecond");
    }

    @Override
    public void onThird() {
    showToast("onThird");
    }


    })
    .setBtnSize(20)
    .setBtnText("i","b","3")
    .show();

    相关回调

    MyDialogListener

    	public abstract void onFirst();//md-确定,ios-第一个
    public abstract void onSecond();//md-取消,ios-第二个
    public void onThird(){}//md-netural,ios-第三个

    public void onCancle(){}

    /**
    * 提供给Input的回调
    * @param input1
    * @param input2
    */

    public void onGetInput(CharSequence input1,CharSequence input2){

    }

    /**
    * 提供给MdSingleChoose的回调
    * @param chosen
    * @param chosenTxt
    */

    public void onGetChoose(int chosen,CharSequence chosenTxt){

    }

    /**
    * 提供给MdMultiChoose的回调
    * @param states
    */

    public void onChoosen( List selectedIndex, List selectedStrs,boolean[] states){

    }

    MyItemDialogListener

     /**
    * IosSingleChoose,BottomItemDialog的点击条目回调
    * @param text
    * @param position
    */

    public abstract void onItemClick(CharSequence text, int position);


    /**
    * BottomItemDialog的底部按钮(经常是取消)的点击回调
    */

    public void onBottomBtnClick(){}


    最后必须调用show(),返回dialog对象

    对话框的消失

    StyledDialog.dismiss(DialogInterface... dialogs);

    两个loading对话框不需要对象就可以直接dismisss:

    StyledDialog.dismissLoading();

    progress dialog 的进度更新

    /**
    * 可以在任何线程调用
    * @param dialog 传入show方法返回的对象
    * @param progress
    * @param max
    * @param msg 如果是转圈圈,会将msg变成msg:78%的形式.如果是水平,msg不起作用
    * @param isHorizontal 是水平线状,还是转圈圈
    */

    public static void updateProgress( Dialog dialog, int progress, int max, CharSequence msg, boolean isHorizontal)


    代码下载: DialogUtil-master.zip

    原文链接:https://github.com/hss01248/DialogUtil

    收起阅读 »

    iOS 微博主页、简书主页、QQ联系人页面等效果

    类似微博主页、简书主页、QQ联系人页面等效果。多页面嵌套,既可以上下滑动,也可以左右滑动切换页面。支持HeaderView悬浮、支持下拉刷新、上拉加载更多。功能特点支持OC与Swift;支持列表懒加载,等到列表真正显示的时候才加载,而不是一次性加载所有列表;支...
    继续阅读 »

    类似微博主页、简书主页、QQ联系人页面等效果。多页面嵌套,既可以上下滑动,也可以左右滑动切换页面。支持HeaderView悬浮、支持下拉刷新、上拉加载更多。

    功能特点

    • 支持OC与Swift;
    • 支持列表懒加载,等到列表真正显示的时候才加载,而不是一次性加载所有列表;
    • 支持首页下拉刷新、列表视图下拉刷新、列表视图上拉加载更多;
    • 支持悬浮SectionHeader的垂直位置调整;
    • 支持从顶部用力往上滚动,下面的列表会跟着滚动,而不会突然卡主,需要使用JXPagerSmoothView类;
    • 列表封装简洁,只要遵从JXPagingViewListViewDelegate协议即可。UIView、UIViewController等都可以;
    • 使用JXCategoryView/JXSegmentedView分类控制器,几乎支持所有主流效果、高度自定义、可灵活扩展;
    • 支持横竖屏切换;
    • 支持点击状态栏滚动当前列表到顶部;
    • 支持列表显示和消失的生命周期方法;
    • isListHorizontalScrollEnabled属性控制列表是否可以左右滑动,默认YES;
    • 支持FDFullscreenPopGesture等全屏手势兼容处理;
    效果图




    安装

    手动

    Swift版本: Clone代码,拖入JXPagingView-Swift文件夹,使用JXPagingView类;

    OC版本: Clone代码,拖入JXPagerView文件夹,使用JXPagerView类;

    CocoaPods

    • Swift版本

    支持swift版本:5.0+

    target '' do
    pod 'JXPagingView/Paging'
    end
    • OC版本
    target '' do
    pod 'JXPagingView/Pager'
    end

    Swift与OC的仓库地址不一样,请注意选择!

    pod repo update然后再pod install


    使用

    swift版本使用类似,只是类名及相关API更改为JXPagingView

    1、初始化JXCategoryTitleViewJXPagerView

    self.categoryView = [[JXCategoryTitleView alloc] initWithFrame:frame];
    //配置categoryView,细节参考源码

    self.pagerView = [[JXPagerView alloc] initWithDelegate:self];
    [self.view addSubview:self.pagerView];

    //⚠️⚠️⚠️将pagerView的listContainerView和categoryView.listContainer进行关联,这样列表就可以和categoryView联动了。⚠️⚠️⚠️
    self.categoryView.listContainer = (id)self.pagerView.listContainerView;

    Swift版本列表关联代码

    //给JXPagingListContainerView添加extension,表示遵从JXSegmentedViewListContainer的协议
    extension JXPagingListContainerView: JXSegmentedViewListContainer {}
    //⚠️⚠️⚠️将pagingView的listContainerView和segmentedView.listContainer进行关联,这样列表就可以和categoryView联动了。⚠️⚠️⚠️
    segmentedView.listContainer = pagingView.listContainerView

    2、实现JXPagerViewDelegate协议

    /**
    返回tableHeaderView的高度,因为内部需要比对判断,只能是整型数
    */
    - (NSUInteger)tableHeaderViewHeightInPagerView:(JXPagerView *)pagerView {
    return JXTableHeaderViewHeight;
    }

    /**
    返回tableHeaderView
    */
    - (UIView *)tableHeaderViewInPagerView:(JXPagerView *)pagerView {
    return self.userHeaderView;
    }


    /**
    返回悬浮HeaderView的高度,因为内部需要比对判断,只能是整型数
    */
    - (NSUInteger)heightForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
    return JXheightForHeaderInSection;
    }


    /**
    返回悬浮HeaderView
    */
    - (UIView *)viewForPinSectionHeaderInPagerView:(JXPagerView *)pagerView {
    return self.categoryView;
    }

    /**
    返回列表的数量
    */
    - (NSInteger)numberOfListsInPagerView:(JXPagerView *)pagerView {
    //和categoryView的item数量一致
    return self.titles.count;
    }

    /**
    根据index初始化一个对应列表实例。注意:一定要是新生成的实例!!!
    只要遵循JXPagerViewListViewDelegate即可,无论你返回的是UIView还是UIViewController都可以。
    */
    - (id)pagerView:(JXPagerView *)pagerView initListAtIndex:(NSInteger)index {
    TestListBaseView *listView = [[TestListBaseView alloc] init];
    if (index == 0) {
    listView.dataSource = @[@"橡胶火箭", @"橡胶火箭炮", @"橡胶机关枪"...].mutableCopy;
    }else if (index == 1) {
    listView.dataSource = @[@"吃烤肉", @"吃鸡腿肉", @"吃牛肉", @"各种肉"].mutableCopy;
    }else {
    listView.dataSource = @[@"【剑士】罗罗诺亚·索隆", @"【航海士】娜美", @"【狙击手】乌索普"...].mutableCopy;
    }
    [listView beginFirstRefresh];
    return listView;
    }

    3、实现JXPagerViewListViewDelegate协议

    列表可以是任意类,UIView、UIViewController等等都可以,只要实现了JXPagerViewListViewDelegate协议就行。

    ⚠️⚠️⚠️一定要保证scrollCallback的正确回调,许多朋友都容易疏忽这一点,导致异常,务必重点注意!

    下面的使用代码参考的是TestListBaseView

    /**
    返回listView。如果是vc包裹的就是vc.view;如果是自定义view包裹的,就是自定义view自己。
    */
    - (UIView *)listView {
    return self;
    }

    /**
    返回listView内部持有的UIScrollView或UITableView或UICollectionView
    主要用于mainTableView已经显示了header,listView的contentOffset需要重置时,内部需要访问到外部传入进来的listView内的scrollView
    */
    - (UIScrollView *)listScrollView {
    return self.tableView;
    }


    /**
    当listView内部持有的UIScrollView或UITableView或UICollectionView的代理方法`scrollViewDidScroll`回调时,需要调用该代理方法传入的callback
    */
    - (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback {
    self.scrollCallback = callback;
    }

    4、列表回调处理

    TestListBaseView在其tableView的滚动回调中,通过调用上面持有的scrollCallback,把列表的滚动事件回调给JXPagerView内部。

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    !self.scrollCallback ?: self.scrollCallback(scrollView);
    }

    常见问题及demo:点击这里



    收起阅读 »

    iOS 下拉刷新控件

    推荐一个iOS的下拉刷新控件 .支持tableview 和collection!先介绍一下基本使用姿势另一种默认下拉刷新和上拉加载更多(通过action)self.tableView.mj_header = [MJRefreshNormalHeader hea...
    继续阅读 »

    推荐一个iOS的下拉刷新控件 .支持tableview 和collection!

    先介绍一下基本使用姿势

    默认下拉刷新和上拉加载更多(通过block)

    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
    //网络请求加载数据完成后在停止刷新
    [self.tableView.mj_header endRefreshing];
    }];
    //这种上拉刷新footer在tableview的底部
    self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
    //网络请求加载数据完成后在停止刷新
    [self.tableView.mj_footer endRefreshing];
    }];
    //另一种上拉刷新,这个上拉刷新footer在tableview最后一条数据的底部
    self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
    //网络请求加载数据完成后在停止刷新
    [self.tableView.mj_footer endRefreshing];
    }];
    //马上进入刷新状态
    [self.tableView.mj_header beginRefreshing];

    另一种默认下拉刷新和上拉加载更多(通过action)

    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(refreshAction)];
    //这种上拉刷新footer在tableview的底部
    self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreAction)];
    //另一种上拉刷新,这个上拉刷新footer在tableview最后一条数据的底部
    self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreAction)];
    这里的前提是得给mj_header、mj_footer赋值。
    self.tableView.mj_header.hidden = YES;//隐藏下拉
    self.tableView.mj_header.hidden = NO;//显示下拉
    self.tableView.mj_footer.hidden = YES;//隐藏上拉
    self.tableView.mj_footer.hidden = NO;//显示上拉

    二、动画图片的下拉刷新和上拉加载

    MJRefreshGifHeader *header = [MJRefreshGifHeader headerWithRefreshingBlock:^{
    NSLog(@"aa");
    }];
    // 设置普通状态的动画图片
    NSMutableArray *idleImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=60; i++) {
    UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_anim__000%zd", I]];
    [idleImages addObject:image];
    }
    [header setImages:idleImages forState:MJRefreshStateIdle];

    // 设置即将刷新状态的动画图片(一松开就会刷新的状态)
    NSMutableArray *refreshingImages = [NSMutableArray array];
    for (NSUInteger i = 1; i<=3; i++) {
    UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"dropdown_loading_0%zd", I]];
    [refreshingImages addObject:image];
    }
    [header setImages:refreshingImages forState:MJRefreshStatePulling];

    // 设置正在刷新状态的动画图片
    [header setImages:refreshingImages forState:MJRefreshStateRefreshing];
    self.tableView.mj_header = header;

    常见与demo下载:点击这里

    源码下载:MJRefresh-3.5.1.zip


    收起阅读 »

    仿QQ未读气泡拖拽效果-BGABadgeView-Android

    效果图Gradle依赖dependencies { implementation 'cn.bingoogolapple:bga-badgeview-api:latestVersion' annotationProcessor "cn.bingoog...
    继续阅读 »

    效果图


    Gradle依赖

    dependencies {
    implementation 'cn.bingoogolapple:bga-badgeview-api:latestVersion'
    annotationProcessor "cn.bingoogolapple:bga-badgeview-compiler:latestVersion"
    }


    初始化控件

    1. 在项目任意一个类上面添加 BGABadge 注解,例如新建一个类 BGABadgeInit 专门用于初始化徽章控件
    2. 需要哪些类具有徽章功能,就把那些类的 Class 作为 BGABadge 注解的参数「下面的代码块给出了例子,不需要的可以删掉对应的行」
    @BGABadge({
    View.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeView,不想用这个类的话就删了这一行
    ImageView.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeImageView,不想用这个类的话就删了这一行
    TextView.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeFloatingTextView,不想用这个类的话就删了这一行
    RadioButton.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeRadioButton,不想用这个类的话就删了这一行
    LinearLayout.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeLinearLayout,不想用这个类的话就删了这一行
    FrameLayout.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeFrameLayout,不想用这个类的话就删了这一行
    RelativeLayout.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeRelativeLayout,不想用这个类的话就删了这一行
    FloatingActionButton.class, // 对应 cn.bingoogolapple.badgeview.BGABadgeFloatingActionButton,不想用这个类的话就删了这一行
    ...
    ...
    ...
    })
    public class BGABadgeInit {
    }
    1. 再 AS 中执行 Build => Rebuild Project
    2. 经过前面三个步骤后就可以通过「cn.bingoogolapple.badgeview.BGABadge原始类名」来使用徽章控件了

    接口说明

    /**
    * 显示圆点徽章
    */
    void showCirclePointBadge();

    /**
    * 显示文字徽章
    *
    * @param badgeText
    */
    void showTextBadge(String badgeText);

    /**
    * 隐藏徽章
    */
    void hiddenBadge();

    /**
    * 显示图像徽章
    *
    * @param bitmap
    */
    void showDrawableBadge(Bitmap bitmap);

    /**
    * 设置拖动删除徽章的代理
    *
    * @param delegate
    */
    void setDragDismissDelegage(BGADragDismissDelegate delegate);

    /**
    * 是否显示徽章
    *
    * @return
    */
    boolean isShowBadge();

    /**
    * 是否可拖动
    *
    * @return
    */
    boolean isDraggable();

    /**
    * 是否正在拖动
    *
    * @return
    */
    boolean isDragging();


    代码下载:BGABadgeView-Android-master.zip

    原文链接:https://github.com/bingoogolapple/BGABadgeView-Android

    收起阅读 »

    iOS 推荐一个项目框架给大家

    最近经常在iOS交流群看到大家有问"有没有什么直接可以用的项目框架啊?""有没有直接创建好结构的框架啊?""有没有什么好用的开源框架啊?"今天就给大家推荐一款Coding-iOS使用方式:项目里用到了 CocoaPods 和 Carthage,如果没有安装的话...
    继续阅读 »

    最近经常在iOS交流群看到大家有问

    "有没有什么直接可以用的项目框架啊?"

    "有没有直接创建好结构的框架啊?"

    "有没有什么好用的开源框架啊?"

    今天就给大家推荐一款

    Coding-iOS

    使用方式:

    项目里用到了 CocoaPods 和 Carthage,如果没有安装的话,需要先自行安装。

    Clone 代码后,初次执行前,需要双击运行根目录下的bootstrap脚本。这个过程涉及到下载依赖,可能会有点久,需耐心等待。

    Tip:由于用到了 submodule,所以必需要把 git 仓库 clone 到本地

    下面介绍一下文件的大概目录先:


    .
    ├── Coding_iOS
    │   ├── Models:数据类
    │   ├── Views:视图类
    │   │   ├── CCell:所有的 CollectionViewCell 都在这里
    │   │   ├── Cell:所有的 TableViewCell 都在这里
    │   │   └── XXX:ListView(项目、动态、任务、讨论、文档、代码)和 InputView(用于聊天和评论的输入框)
    │   ├── Controllers:控制器,对应app中的各个页面
    │   │   ├── Login:登录页面
    │   │   ├── RootControllers:登录后的根页面
    │   │   ├── MeSetting:设置信息页面
    │   │   └── XXX:其它页面
    │   ├── Images:app 中用到的所有的图片都在这里
    │   ├── Resources:资源文件
    │   ├── Util:一些常用控件和 Category、Manager 之类
    │   │   ├── Common
    │   │   ├── Manager
    │   │   ├── OC_Category
    │   │   └── ObjcRuntime
    │   └── Vendor:用到的一些第三方类库,一般都有改动
    │      ├── AFNetworking
    │      ├── AGEmojiKeyboard
    │      ├── ASProgressPopUpView
    │      ├── ActionSheetPicker
    │      ├── FontAwesome+iOS
    │      ├── MJPhotoBrowser
    │      ├── MLEmojiLabel
    │      ├── NSDate+Helper
    │      ├── NSStringEmojize
    │      ├── PPiAwesomeButton
    │      ├── QBImagePickerController
    │      ├── RDVTabBarController
    │      ├── SMPageControl
    │      ├── SVPullToRefresh
    │      ├── SWTableViewCell
    │      ├── UMENG
    │      ├── UMessage_Sdk_1.1.0
    │      ├── XGPush
    │      ├── XTSegmentControl
    │    └── iCarousel
    └── Pods:项目使用了 [CocoaPods](http://code4app.com/article/cocoapods-install-usage) 这个类库管理工具


    再说下项目的启动流程:

    在 AppDelegate 的启动方法中,先设置了一下 Appearance 的样式,然后根据用户的登录状态选择是去加载登录页面 LoginViewController,还是登录后的 RootTabViewController 页面。

    RootTabViewController 继承自第三方库 RDVTabBarController。在 RootTabViewController 里面依次加载了 Project_RootViewController、MyTask_RootViewController、Tweet_RootViewController、Message_RootViewController、Me_RootViewController 五个 RootViewController,后续的页面跳转都是基于这几个 RootViewController 引过去的。

    项目里面还有些需要注意的点

    • Coding_NetAPIManager:基本上 app 的所有请求接口都放在了这里。网络请求使用的是 AFNetworking 库,与服务器之间的数据交互格式用的都是 json(与 Coding 使用的 api 一致)。

    • 关于推送:刚开始是用的 友盟推送,后来又改用了 腾讯信鸽,因为要兼顾旧版本 app 的推送,所以服务器是同时保留了两套推送。但是为了确保新版本的 app 不同时收到双份相同的推送消息,所以当前代码里还存留了友盟的 sdk,用于解除推送 token 与友盟 Alias 的绑定。

    • 关于 ProjectViewController:这个就是进入到某个项目之后的页面,这里包含了项目的动态、任务、讨论、文档、代码、成员各类信息,而且每类信息里面还可能会有新的分类(如‘任务’里面还分有各个成员的任务);这个页面相当的臃肿,我对它们做了拆分,都放在视图类 Views 目录下面。 首先是把数据列表独立成了对应的 XXXListView(如 ProjectTaskListView);然后如果需要标签切换的话,会再新建一个 XXXsView(如:ProjectTasksView),在这个视图中,上面会放一个切换栏 XTSegmentControl 显示各个标签,下面放一个 iCarousel 可以滑动显示各个标签的内容;最后这些视图都会存储在 ProjectViewController 的 projectContentDict 变量里面,根据顶部导航栏选择的类别,去显示或隐藏对应的视图。

    • 关于 UIMessageInputView:这个是私信聊天的输入框。因为这个输入框好多地方都有用到(冒泡、任务、讨论的评论还有私信),所以这个输入框就写成了一个相对独立的控件,并且直接显示在了 keyWindow 里面而不是某个视图里。这里的表情键盘用的是 AGEmojiKeyboard 改写了一下。

    • 关于 Emoji:这个,Coding 站点的 emoji 都是用的图片,而且服务器是不接受大部分 emoji 字符的,所以刚开始的时候 app 一直不能处理 emoji 表情;又因为没有 emoji 图片名和 emoji code 码的对应关系表,所以拖了很久都没能做好转换。直到在 github 上面找到了 NSStringEmojize 这个项目;试了一下,虽然也不能全部解析,但是大部分表情都能正确显示了,不能更感谢。

    • 关于如何正确显示冒泡的内容:api 返回的数据里面,冒泡内容都是 html 格式,需要做一下预处理;其实私信、讨论里面的内容也是 html。解析 html 的类名是 HtmlMediaItem,它是先用 hpple 对 html 进行了解析,然后把对应的 media 元素和对应的位置做一个存储,显示的时候便可以根据需要来显示了。

    最后说下 CocoaPods 里面用到的第三方类库


    如有其他使用疑问请点击 常见使用问题及源码





    收起阅读 »

    图片浏览缩放控件-PhotoView

    PhotoView 图片浏览缩放控件一个流畅的photoview#注意 由于facebook的Fresco图片加载组件所加载出来的drawable图片并非真实的drawable,无法直接获取图片真实宽高,也无法直接响应ImageMatrix的变换, 且根据Fr...
    继续阅读 »

    PhotoView 图片浏览缩放控件

    一个流畅的photoview

    #注意 由于facebook的Fresco图片加载组件所加载出来的drawable图片并非真实的drawable,无法直接获取图片真实宽高,也无法直接响应ImageMatrix的变换, 且根据Fresco文档的介绍,在后续的版本中,DraweeView会直接继承自View,所有暂不考虑支持Fresco。 对于其他第三方图片加载库如Glide,ImageLoader,xUtils都是支持的

    #使用 1.Gradle添加依赖 (推荐)

    dependencies {
    compile 'com.bm.photoview:library:1.4.1'
    }

    (或者也可以将项目下载下来,将Info.java和PhotoView.java两个文件拷贝到你的项目中,不推荐)

    2.xml添加

     <com.bm.library.PhotoView
    android:id="@+id/img"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="centerInside"
    android:src="@drawable/bitmap1" />

    3.java代码

    PhotoView photoView = (PhotoView) findViewById(R.id.img);
    // 启用图片缩放功能
    photoView.enable();
    // 禁用图片缩放功能 (默认为禁用,会跟普通的ImageView一样,缩放功能需手动调用enable()启用)
    photoView.disenable();
    // 获取图片信息
    Info info = photoView.getInfo();
    // 从普通的ImageView中获取Info
    Info info = PhotoView.getImageViewInfo(ImageView);
    // 从一张图片信息变化到现在的图片,用于图片点击后放大浏览,具体使用可以参照demo的使用
    photoView.animaFrom(info);
    // 从现在的图片变化到所给定的图片信息,用于图片放大后点击缩小到原来的位置,具体使用可以参照demo的使用
    photoView.animaTo(info,new Runnable() {
    @Override
    public void run() {
    //动画完成监听
    }
    });
    // 获取/设置 动画持续时间
    photoView.setAnimaDuring(int during);
    int d = photoView.getAnimaDuring();
    // 获取/设置 最大缩放倍数
    photoView.setMaxScale(float maxScale);
    float maxScale = photoView.getMaxScale();
    // 设置动画的插入器
    photoView.setInterpolator(Interpolator interpolator);

    代码下载:PhotoView-master.zip

    原文链接:https://github.com/bm-x/PhotoView

    收起阅读 »

    高度自定义、支持周视图的日历控件-CalendarView

    CalendarView使用详细文档日历控件定制是移动开发平台上比较常见的而且比较难的需求,一般会遇到以下问题:性能差,加载速度慢,原因是各种基于GridView或RecyclerView等ViewGroup实现的日历,控件数太多,假设一个月视图界面有42个i...
    继续阅读 »

    CalendarView使用详细文档

    日历控件定制是移动开发平台上比较常见的而且比较难的需求,一般会遇到以下问题:

    • 性能差,加载速度慢,原因是各种基于GridView或RecyclerView等ViewGroup实现的日历,控件数太多,假设一个月视图界面有42个item,每个item里面分别就有2个子TextView:天数、农历数和本身3个控件,这样一个月视图就有42 * 3+1(RecyclerView or GridView),清楚ViewPager特性的开发者就会明白,一般ViewPager持有3个item,那么一个日历控件持有的View控件数的数量将达到 1(ViewPager)+ 3(RecyclerView or GridView) + 3 * 42 * 3 = 382,如果用1个View来代替RecyclerView等,用Canvas来代替各种TextView,那View的数量瞬间将下降360+,内存和性能优势将相当明显了
    • 难定制 一般日历框架发布的同时也将UI风格确定下来了,假如人人都使用这个日历框架,那么将会千篇一律,难以突出自己的风格,要么就得改源码,成本太大,不太实际
    • 功能性不足 例如无法自定义周起始、无法更改选择模式、动态设置UI等等
    • 无法满足产品经理提出的变态需求 今天产品经历说我们要这样的实现、明天跟你说这里得改、后天说我们得限制一些日期...

    但现在有了全新的 CalendarView 控件,它解锁了各种姿势,而且你可以任意定制,直到你满足为止...

    插拔式设计

    插拔式设计:好比插座一样,插上灯泡就会亮,插上风扇就会转,看用户需求什么而不是看插座有什么,只要是电器即可。此框架使用插拔式,既可以在编译时指定年月日视图,如:app:month_view="xxx.xxx.MonthView.class",也可在运行时动态更换年月日视图,如:CalendarView.setMonthViewClass(MonthView.Class),从而达到UI即插即用的效果,相当于框架不提供UI实现,让UI都由客户端实现,不至于日历UI都千篇一律,只需遵守插拔式接口即可随意定制,自由化程度非常高。

    CalendarView 的特性

    • 基于Canvas绘制,极速性能
    • 热插拔思想,任意定制周视图、月视图,即插即用!
    • 支持单选、多选、范围选择、国内手机日历默认自动选择等选择模式
    • 支持静态、动态设置周起始,一行代码搞定
    • 支持静态、动态设置日历项高度、日历填充模式
    • 支持设置任意日期范围、任意拦截日期
    • 支持多点触控、手指平滑切换过渡,拒绝界面抖动
    • 类NestedScrolling特性,嵌套滚动
    • 既然这么多支持,那一定支持英语、繁体、简体,任意定制实现

    注意: 框架本身只是实现各自逻辑,不实现UI,UI如同一张白纸,任凭客户端自行通过Canvas绘制实现,如果不熟悉Canvas的,请自行了解各自Canvas.drawXXX方法,UI都靠Canvas实现,坐标都已经计算好了,因此怎么隐藏农历,怎么换某些日期的字,这些都不属于框架范畴,只要你想换,都能随便换。

    再次注意: app Demo只是Demo,只是示例如何使用,与框架本身没有关联,不属于框架一部分

    接下来请看CalendarView操作,前方高能

    • 你这样继承自己的月视图和周视图,只需要依次实现绘制选中:onDrawSelected、绘制事务:onDrawScheme、绘制文本:onDrawText 这三个回调即可,参数和坐标都已经在回调函数上实现好,周视图也是一样的逻辑,只是不需要y参数
    /**
    * 定制高仿魅族日历界面,按你的想象力绘制出各种各样的界面
    *
    */
    public class MeiZuMonthView extends MonthView {

    /**
    * 绘制选中的日子
    *
    * @param canvas canvas
    * @param calendar 日历日历calendar
    * @param x 日历Card x起点坐标
    * @param y 日历Card y起点坐标
    * @param hasScheme hasScheme 非标记的日期
    * @return 返回true 则绘制onDrawScheme,因为这里背景色不是是互斥的,所以返回true
    */
    @Override
    protected boolean onDrawSelected(Canvas canvas, Calendar calendar, int x, int y, boolean hasScheme) {
    //这里绘制选中的日子样式,看需求需不需要继续调用onDrawScheme
    return true;
    }

    /**
    * 绘制标记的事件日子
    *
    * @param canvas canvas
    * @param calendar 日历calendar
    * @param x 日历Card x起点坐标
    * @param y 日历Card y起点坐标
    */
    @Override
    protected void onDrawScheme(Canvas canvas, Calendar calendar, int x, int y) {
    //这里绘制标记的日期样式,想怎么操作就怎么操作
    }

    /**
    * 绘制文本
    *
    * @param canvas canvas
    * @param calendar 日历calendar
    * @param x 日历Card x起点坐标
    * @param y 日历Card y起点坐标
    * @param hasScheme 是否是标记的日期
    * @param isSelected 是否选中
    */
    @Override
    protected void onDrawText(Canvas canvas, Calendar calendar, int x, int y, boolean hasScheme, boolean isSelected) {
    //这里绘制文本,不要再问我怎么隐藏农历了,不要再问我怎么把某个日期换成特殊字符串了,要怎么显示你就在这里怎么画,你不画就不显示,是看你想怎么显示日历的,而不是看框架
    }
    }
    • 当你实现好之后,直接在xml界面上添加特性,编译后可以即时预览效果:
    <attr name="month_view" format="string" />
    <attr name="week_view" format="string" />

    app:month_view="com.haibin.calendarviewproject.MeiZuMonthView"
    app:week_view="com.haibin.calendarviewproject.MeiZuWeekView"
    • 视图有多种模式可供选择,几乎涵盖了各种需求,看各自的需求自行继承
    如果继承这2个,MonthView、WeekView,即select_mode="default_mode",这是默认的手机自带的日历模式,会自动选择月的第一天,不支持拦截器,
    也可以设置select_mode="single_mode",即单选模式,支持拦截器

    如果继承这2个,RangeMonthView、RangeWeekView,即select_mode="range_mode",这是范围选择模式,支持拦截器

    如果继承这2个,MultiMonthView、MultiWeekView,即select_mode="multi_mode",这是多选模式,支持拦截器
    • 如果静态模式无法满足你的需求,你可能需要动态变换定制的视图界面,你可以使用热插拔特性,即插即用,不爽就换:
    mCalendarView.setWeekView(MeiZuWeekView.class);

    mCalendarView.setMonthView(MeiZuMonthView.class);
    • 如果你需要可收缩的日历,你可以在 CalendarView 父布局添加 CalendarLayout,当然你不需要周视图也可以不用,例如原生日历,使用如下:
    <com.haibin.calendarview.CalendarLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    app:default_status="shrink"
    app:calendar_show_mode="only_week_view"
    app:calendar_content_view_id="@+id/recyclerView">

    <com.haibin.calendarview.CalendarView
    android:id="@+id/calendarView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#fff"
    app:month_view="com.haibin.calendarviewproject.simple.SimpleMonthView"
    app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
    app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"
    app:calendar_height="50dp"
    app:current_month_text_color="#333333"
    app:current_month_lunar_text_color="#CFCFCF"
    app:min_year="2004"
    app:other_month_text_color="#e1e1e1"
    app:scheme_text=""
    app:scheme_text_color="#333"
    app:scheme_theme_color="#333"
    app:selected_text_color="#fff"
    app:selected_theme_color="#333"
    app:week_start_with="mon"
    app:week_background="#fff"
    app:month_view_show_mode="mode_only_current"
    app:week_text_color="#111" />

    <android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff" />
    com.haibin.calendarview.CalendarLayout>
    • 使用可收缩的日历你可以使用监听器,监听视图变换
    public void setOnViewChangeListener(OnViewChangeListener listener);
    • 当然 CalendarLayout 有很多特性可提供周月视图无缝切换,而且,平滑手势不抖动!使用 CalendarLayout,你需要指定 calendar_content_view_id,用他来平移收缩月视图,更多特性如下:

    <attr name="calendar_show_mode">
    <enum name="both_month_week_view" value="0" />
    <enum name="only_week_view" value="1" />
    <enum name="only_month_view" value="2" />
    attr>

    <attr name="default_status">
    <enum name="expand" value="0" />
    <enum name="shrink" value="1" />
    attr>

    <attr name="calendar_content_view_id" format="integer" />
    • CalendarView 可以设置全屏,只需设置 app:calendar_match_parent="true"即可,全屏CalendarView是不需要周视图的,不必嵌套CalendarLayout

    • CalendarView 也提供了高效便利的年视图,可以快速切换年份、月份,十分便利

    • 但年视图也不一定就适合你的胃口,如果你希望像弹出 DatePickerView,通过它来跳转日期,你可以使用以下的API来让日历与其它控件联动

    CalendarView.scrollToCalendar();

    CalendarView.scrollToNext();

    CalendarView.scrollToPre();

    CalendarView.scrollToXXX();

    • 你也许需要像魅族日历一样,可以静态、动态更换周起始

    app:week_start_with="mon、sun、sat"

    CalendarView.setWeekStarWithSun();

    CalendarView.setWeekStarWithMon();

    CalendarView.setWeekStarWithSat();

    • 假如你是做酒店、旅游等应用场景的APP的,那么需要可选范围的日历,你可以这样继承,和普通视图实现完全一样
    public class CustomRangeMonthView extends RangeMonthView{

    }

    public class CustomRangeWeekView extends RangeWeekView{

    }
    • 然后你需要设置选择模式为范围模式:select_mode="range_mode"

    • 酒店式日历场景当然是不能从昨天开始订房的,也不能无限期订房,所以你需要静态或动态设置日历范围、精确到具体某一天!!!









    CalendarView.setRange(int minYear, int minYearMonth, int minYearDay,
    int maxYear, int maxYearMonth, int maxYearDay)

    • 当然还有更特殊的日子也是不能选择的,例如:某月某号起这N天时间内因为超强台风来袭,酒店需停止营业N天,这段期间不可订房,这时日期拦截器就排上用场了
    //设置日期拦截事件
    mCalendarView.setOnCalendarInterceptListener(new CalendarView.OnCalendarInterceptListener() {
    @Override
    public boolean onCalendarIntercept(Calendar calendar) {
    //这里写拦截条件,返回true代表拦截,尽量以最高效的代码执行
    return calendar.isWeekend();
    }

    @Override
    public void onCalendarInterceptClick(Calendar calendar, boolean isClick) {
    //todo 点击拦截的日期回调
    }
    });
    • 添加日期拦截器和范围设置后,你可以在周月视图按需求获得他们的结果
    boolean isInRange = isInRange(calendar);//日期是否在范围内,超出范围的可以置灰

    boolean isEnable = !onCalendarIntercept(calendar);//日期是否可用,没有被拦截,被拦截的可以置灰

    • 假如你是做清单类、任务类APP的,可能会有这样的需求:标记某天事务的进度,这也很简单,因为:日历界面长什么样,你自己说了算!!!

    • 也许你只需要像原生日历那样就够了,但原生日历那奇怪且十分不友好的style,受到theme的影响,各种头疼,使用此控件,你只需要简简单单定制月视图就够了,CalendarView 能非常简单就高仿各种日历UI

    • CalendarView 提供了 setSchemeDate(Map mSchemeDates) 这个十分高效的API用来动态标记事务,即时你的数据量达到数千、数万、数十万,都不会对UI渲染造成影响

    • 日历类 Calendar 提供了许多十分有用的API

    boolean isWeekend();//判断是不是周末,可以用不同的画笔绘制周末的样式

    int getWeek();//获取星期

    String getSolarTerm();//获取24节气,可以用不同颜色标记不同节日

    String getGregorianFestival();//获取公历节日,自由判断,把节日换上喜欢的颜色

    String getTraditionFestival();//获取传统节日

    boolean isLeapYear();//是否是闰年

    int getLeapMonth();//获取闰月

    boolean isSameMonth(Calendar calendar);//是否相同月

    int compareTo(Calendar calendar);//比较日期大小 -1 0 1

    long getTimeInMillis();//获取时间戳

    int differ(Calendar calendar);//日期运算,相差多少天

    CalendarView 的全部xml特性如下:

    <declare-styleable name="CalendarView">

    <attr name="calendar_padding" format="dimension" />

    <attr name="month_view" format="color" />
    <attr name="week_view" format="string" />
    <attr name="week_bar_height" format="dimension" />
    <attr name="week_bar_view" format="color" />
    <attr name="week_line_margin" format="dimension" />

    <attr name="week_line_background" format="color" />
    <attr name="week_background" format="color" />
    <attr name="week_text_color" format="color" />
    <attr name="week_text_size" format="dimension" />

    <attr name="current_day_text_color" format="color" />
    <attr name="current_day_lunar_text_color" format="color" />

           <attr name="calendar_height" format="string" />
    <attr name="day_text_size" format="string" />
    <attr name="lunar_text_size" format="string" />

    <attr name="scheme_text" format="string" />
    <attr name="scheme_text_color" format="color" />
    <attr name="scheme_month_text_color" format="color" />
    <attr name="scheme_lunar_text_color" format="color" />

    <attr name="scheme_theme_color" format="color" />

    <attr name="selected_theme_color" format="color" />
    <attr name="selected_text_color" format="color" />
    <attr name="selected_lunar_text_color" format="color" />

    <attr name="current_month_text_color" format="color" />
    <attr name="other_month_text_color" format="color" />

    <attr name="current_month_lunar_text_color" format="color" />
    <attr name="other_month_lunar_text_color" format="color" />


    <attr name="year_view_month_text_size" format="dimension" />
    <attr name="year_view_day_text_size" format="dimension" />
    <attr name="year_view_month_text_color" format="color" />
    <attr name="year_view_day_text_color" format="color" />
    <attr name="year_view_scheme_color" format="color" />

    <attr name="min_year" format="integer" />  
     <attr name="max_year" format="integer" />
    <attr name="min_year_month" format="integer" />
    <attr name="max_year_month" format="integer" />


    <attr name="month_view_scrollable" format="boolean" />

    <attr name="week_view_scrollable" format="boolean" />

    <attr name="year_view_scrollable" format="boolean" />
           

    <attr name="month_view_show_mode">
    <enum name="mode_all" value="0" /> 
    <enum name="mode_only_current" value="1" />
    <enum name="mode_fix" value="2" />
    attr>


    <attr name="week_start_with">
    <enum name="sun" value="1" />
    <enum name="mon" value="2" />
    <enum name="sat" value="7" />
    attr>


    <attr name="select_mode">
    <enum name="default_mode" value="0" />
    <enum name="single_mode" value="1" />
    <enum name="range_mode" value="2" />
    <enum name="multi_mode" value="3" />
    attr>


    <attr name="max_multi_select_size" format="integer" />


    <attr name="min_select_range" format="integer" />
    <attr name="max_select_range" format="integer" />


    <attr name="month_view_auto_select_day">
    <enum name="first_day_of_month" value="0" />
    <enum name="last_select_day" value="1" />
    <enum name="last_select_day_ignore_current" value="2" />
    attr>
    declare-styleable>

    写在最后,其它各种场景姿势就不多说了,看自己需求去实现。再次注意:Demo只是Demo,只是示例如何使用,与框架本身没有关联,不属于框架一部分


    代码下载: CalendarView-master.zip

    原文链接:https://github.com/huanghaibin-dev/CalendarView


    收起阅读 »

    iOS视频播放器

    非常棒的视频播放器 目前支持的功能如下:普通模式的播放,类似于腾讯视频、爱奇艺等APP;列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;列表视频滑出屏...
    继续阅读 »

    非常棒的视频播放器 

    目前支持的功能如下:

    • 普通模式的播放,类似于腾讯视频、爱奇艺等APP;
    • 列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;
    • 列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;
    • 列表视频滑出屏幕后停止播放、滑出屏幕后小窗播放;
    • 优雅的全屏,支持横屏和竖屏全屏模式



    支持多种效果播放, 并且支持ijk 

    使用方式:

    因为作者将库更新的相似于组件化 ,所以尽量直接pod使用 .如不需要ijk相关功能, 直接pod如下三个库

    pod 'ZFPlayer', '~> 4.0'
    pod 'ZFPlayer/ControlView', '~> 4.0'
    pod 'ZFPlayer/AVPlayer', '~> 4.0'

    如果需要使用ijk 那么在添加

    pod 'ZFPlayer/ijkplayer', '~> 4.0'


    默认播放器使用方式代码

    #import

    #import

    #import


    @property (nonatomic, strong) ZFPlayerController *player;

    @property (nonatomic, strong) ZFPlayerControlView *controlView;



    ZFAVPlayerManager *playerManager = [[ZFAVPlayerManager alloc] init];

        playerManager.shouldAutoPlay = YES;


        self.player = [ZFPlayerController playerWithPlayerManager:playerManager containerView:self.containerView];

        self.controlView.portraitControlView.fullScreenBtn.hidden = YES;

        self.player.controlView = self.controlView;

        

        /// 设置退到后台继续播放

        self.player.pauseWhenAppResignActive = NO;

        @zf_weakify(self)

        /// 播放完成

        self.player.playerDidToEnd = ^(id  _Nonnull asset) {

            @zf_strongify(self)

        // your code


        };

        // 设置播放地址(支持沙盒路径和网络路径)

        self.player.assetURL = self.playUrl;

        [self.controlView showTitle:@"标题" coverURLString:@"videoRrlString" fullScreenMode:ZFFullScreenModeAutomatic];



    - (ZFPlayerControlView *)controlView {

        if (!_controlView) {

            _controlView = [ZFPlayerControlView new];

            _controlView.fastViewAnimated = YES;

            _controlView.autoHiddenTimeInterval = 5;

            _controlView.autoFadeTimeInterval = 0.5;

            _controlView.prepareShowLoading = YES;

            _controlView.prepareShowControlView = NO;

        }

        return _controlView;

    }




    这样就可以实现一个类似于腾讯视频/爱奇艺的播放器啦



    具体中文说明可阅读 : ZFPlayer中文深度说明

    具体英文说明及Demo下载:常见问题与Demo下载





    收起阅读 »

    iOS网络请求库

    pod 集成pod 'AFNetworking', '~> 4.0'或者下载附件直接添加到项目当中AFNetworking.zip 4.0AFN3.x.zip 3.x作为一个iOS开发者耳熟能详的网络请求库&n...
    继续阅读 »

    pod 集成


    pod 'AFNetworking''~> 4.0'



    或者下载附件直接添加到项目当中

    AFNetworking.zip 4.0

    AFN3.x.zip 3.x

    作为一个iOS开发者耳熟能详的网络请求库 .具体用法不在赘述这里简单介绍一下使用中的技巧吧(因为目前普遍使用的是3.x的版本,所以技巧主要适用于3.x )

    正常用法:

    [[AFHTTPSessionManager managerPOST:@"port"parameters:nilprogress:^(NSProgress * _Nonnull uploadProgress) {

                

            } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

                

            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

                

            }];


    可其实AFNmanager方法只是伪单例我们完全可以基于AFN进行一次二次封装做成真单例

    :ZyNetWorking.h

    **

     *  请求成功所走方法

     *

     *  @param responseObject 请求返还的数据

     */

    typedef void (^ZuYResponseSuccess)(NSURLSessionDataTask * task,id responseObject,NSInteger code);



    /**

     *  请求错误所走方法

     *

     *  @param error 请求错误返还的信息

     */

    typedef void (^ZYResponseFail)(NSURLSessionDataTask * task, NSError * error);



    typedef void (^ZYProgress)(NSProgress *progress);

    @interfaceZyNetworking : NSObject


    + (instancetype)shareInstance;


    -(void)POST:(NSString *)url

         params:(NSDictionary *__nullable)params

        success:(ZuYResponseSuccess)success

           fail:(ZYResponseFail)fail;

    ZyNetWorking.m

    @interfaceZyNetworking ()

    @property (nonatomicstrongAFHTTPSessionManager *manager;

    @end

    @implementationZyNetworking

    staticZyNetworking *_instance = nil;

    + (instancetype)shareInstance

    {

        static dispatch_once_t onceToken ;

        dispatch_once(&onceToken, ^{

            _instance = [[super allocWithZone:NULLinit];

            _instance.manager = [AFHTTPSessionManagermanager];

            _instance.manager.responseSerializer.acceptableContentTypes = [NSSetsetWithObjects:@"text/plain",@"application/json"@"text/json"@"text/javascript",nil];

            _instance.manager.requestSerializer.timeoutInterval = 30.f;// 请求超时时间

        }) ;

        return_instance;

    }

     

    -(void)POST:(NSString *)url params:( NSDictionary *__nullable)params

        success:(ZuYResponseSuccess)success fail:(ZYResponseFail)fail{

         

         //如果需要请求token权鉴 ,可也在这里进行统一处理

        [_instance.manager.requestSerializersetValue:token forHTTPHeaderField:@"token"];

        

            [_instance.manager POST:url parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

         //your code 

         //根据你的业务需求进行你想要的处理

         //甚至可以配合测试业务处理请求日志



                NSInteger code = [[responseObject objectForKey:@"code"integerValue];


                success(task,responseObject,code);


                

            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

                

                NSHTTPURLResponse * responses = (NSHTTPURLResponse *)task.response;

             // your code . 

             //示例根据失败也业务码进行不同的处理 .例如401 需要重新请求token   402强制退出登录等 . 也可记录请求日志(错误)

             // 如不需要也可直接做 fail(task,error)的回调

                if (responses.statusCode == 401) {

                 

                    [self zuyuTokenRefresh:url params:params];

                   

                }else if (responses.statusCode == 402) {

                    

                }else{

                    fail(task,error);

                }

            }]; 

        

    }

     



    git地址:https://github.com/AFNetworking/AFNetworking

    收起阅读 »

    加载反馈页管理框架-LoadSir

    LoadSirLoadSir是一个高效易用,低碳环保,扩展性良好的加载反馈页管理框架,在加载网络或其他数据时候,根据需求切换状态页面, 可添加自定义状态页面,如加载中,加载失败,无数据,网络超时,如占位图,登录失效等常用页面。可配合网络加载框架,结合返回 状态...
    继续阅读 »

    LoadSir

    LoadSir是一个高效易用,低碳环保,扩展性良好的加载反馈页管理框架,在加载网络或其他数据时候,根据需求切换状态页面, 可添加自定义状态页面,如加载中,加载失败,无数据,网络超时,如占位图,登录失效等常用页面。可配合网络加载框架,结合返回 状态码,错误码,数据进行状态页自动切换,封装使用效果更佳


    使用场景



    流程图



    LoadSir的功能及特点

    • 支持Activity,Fragment,Fragment(v4),View状态回调
    • 适配多个Fragment切换,及Fragment+ViewPager切换,不会布局叠加或者布局错乱
    • 利用泛型转换输入信号和输出状态,可根据网络返回体的状态码或者数据返回自动适配状态页,实现全局自动状态切换
    • 无需修改布局文件
    • 只加载唯一一个状态视图,不会预加载全部视图
    • 不需要设置枚举或者常量状态值,直接用状态页类类型(xxx.class)作为状态码
    • 可对单个状态页单独设置点击事件,根据返回boolean值覆盖或者结合OnReloadListener使用,如网络错误可跳转设置页
    • 无预设页面,低耦合,开发者随心配置
    • 可保留标题栏(Toolbar,titile view等)
    • 可设置重新加载点击事件(OnReloadListener)
    • 可自定义状态页(继承Callback类)
    • 可在子线程直接切换状态
    • 可设置初始状态页(常用进度页作为初始状态)
    • 可扩展状态页面,在配置中添加自定义状态页
    • 可全局单例配置,也可以单独配置

    开始使用LoadSir

    LoadSir的使用,只需要简单的三步

    添加依赖

    compile 'com.kingja.loadsir:loadsir:1.3.8'

    第一步:配置

    全局配置方式

    全局配置方式,使用的是单例模式,即获取的配置都是一样的。可在Application中配置,添加状态页,设置默认状态页

    public class App extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    LoadSir.beginBuilder()
    .addCallback(new ErrorCallback())//添加各种状态页
    .addCallback(new EmptyCallback())
    .addCallback(new LoadingCallback())
    .addCallback(new TimeoutCallback())
    .addCallback(new CustomCallback())
    .setDefaultCallback(LoadingCallback.class)//设置默认状态页
    .commit()
    ;
    }
    }
    单独配置方式

    如果你即想保留全局配置,又想在某个特殊页面加点不同的配置,可采用该方式。

    LoadSir loadSir = new LoadSir.Builder()
    .addCallback(new LoadingCallback())
    .addCallback(new EmptyCallback())
    .addCallback(new ErrorCallback())
    .build();
    loadService = loadSir.register(this, new Callback.OnReloadListener() {
    @Override
    public void onReload(View v) {
    // 重新加载逻辑
    }
    });

    第二步:注册

    在Activity中使用
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_content);
    // Your can change the callback on sub thread directly.
    LoadService loadService = LoadSir.getDefault().register(this, new Callback.OnReloadListener() {
    @Override
    public void onReload(View v) {
    // 重新加载逻辑
    }
    });
    }}
    在View 中使用
    ImageView imageView = (ImageView) findViewById(R.id.iv_img);
    LoadSir loadSir = new LoadSir.Builder()
    .addCallback(new TimeoutCallback())
    .setDefaultCallback(LoadingCallback.class)
    .build()
    ;
    loadService = loadSir.register(imageView, new Callback.OnReloadListener() {
    @Override
    public void onReload(View v) {
    loadService.showCallback(LoadingCallback.class);
    // 重新加载逻辑
    }
    });
    Ps:
    [1]要注册RelativeLayoutConstraintLayout的子View,如果该子View被其它子View约束,建议在子View外层再包一层布局,参考
    acitivy_view.xm和activity_constraintlayout.xml
    在Fragment 中使用

    由于Fragment添加到Activitiy方式多样,比较特别,所以在Fragment注册方式不同于上面两种,大家先看模板代码:

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle
    savedInstanceState) {
    //第一步:获取布局View
    rootView = View.inflate(getActivity(), R.layout.fragment_a_content, null);
    //第二步:注册布局View
    LoadService loadService = LoadSir.getDefault().register(rootView, new Callback.OnReloadListener() {
    @Override
    public void onReload(View v) {
    // 重新加载逻辑
    }
    });
    //第三步:返回LoadSir生成的LoadLayout
    return loadService.getLoadLayout();
    }

    第三步: 回调

    直接回调
    protected void loadNet() {
    // 进行网络访问...
    // 进行回调
    loadService.showSuccess();//成功回调
    loadService.showCallback(EmptyCallback.class);//其他回调
    }
    转换器回调 (推荐使用)

    如果你不想再每次回调都要手动进行的话,可以选择注册的时候加入转换器,可根据返回的数据,适配对应的状态页。

    LoadService loadService = LoadSir.getDefault().register(this, new Callback.OnReloadListener() {
    @Override
    public void onReload(View v) {
    // 重新加载逻辑
    }}, new Convertor<HttpResult>() {
    @Override
    public ClassCallback> map(HttpResult httpResult) {
    ClassCallback> resultCode = SuccessCallback.class;
    switch (httpResult.getResultCode()) {
    case SUCCESS_CODE://成功回调
    if (httpResult.getData().size() == 0) {
    resultCode = EmptyCallback.class;
    }else{
    resultCode = SuccessCallback.class;
    }
    break;
    case ERROR_CODE:
    resultCode = ErrorCallback.class;
    break;
    }
    return resultCode;
    }
    });

    回调的时候直接传入转换器指定的数据类型。

    loadService.showWithConvertor(httpResult);

    自定义回调页

    LoadSir为了完全解耦,没有预设任何状态页,需要自己实现,开发者自定义自己的回调页面,比如加载中,没数据,错误,超时等常用页面, 设置布局及自定义点击逻辑

    public class CustomCallback extends Callback {

    //填充布局
    @Override
    protected int onCreateView() {
    return R.layout.layout_custom;
    }
    //当前Callback的点击事件,如果返回true则覆盖注册时的onReloa(),如果返回false则两者都执行,先执行onReloadEvent()。
    @Override
    protected boolean onReloadEvent(final Context context, View view) {
    Toast.makeText(context.getApplicationContext(), "Hello buddy! :p", Toast.LENGTH_SHORT).show();
    (view.findViewById(R.id.iv_gift)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    Toast.makeText(context.getApplicationContext(), "It's your gift! :p", Toast.LENGTH_SHORT).show();
    }
    });
    return true;
    }

    //是否在显示Callback视图的时候显示原始图(SuccessView),返回true显示,false隐藏
    @Override
    public boolean getSuccessVisible() {
    return super.getSuccessVisible();
    }

    //将Callback添加到当前视图时的回调,View为当前Callback的布局View
    @Override
    public void onAttach(Context context, View view) {
    super.onAttach(context, view);
    }

    //将Callback从当前视图删除时的回调,View为当前Callback的布局View
    @Override
    public void onDetach() {
    super.onDetach(context, view);
    }

    }

    动态修改Callback

    loadService = LoadSir.getDefault().register(...);
    loadService.setCallBack(EmptyCallback.class, new Transport() {
    @Override
    public void order(Context context, View view) {
    TextView mTvEmpty = (TextView) view.findViewById(R.id.tv_empty);
    mTvEmpty.setText("fine, no data. You must fill it!");
    }
    });

    LoadSir自带便携式Callback

    ProgressCallback loadingCallback = new ProgressCallback.Builder()
    .setTitle("Loading", R.style.Hint_Title)
    .build();

    HintCallback hintCallback = new HintCallback.Builder()
    .setTitle("Error", R.style.Hint_Title)
    .setSubTitle("Sorry, buddy, I will try it again.")
    .setHintImg(R.drawable.error)
    .build();

    LoadSir loadSir = new LoadSir.Builder()
    .addCallback(loadingCallback)
    .addCallback(hintCallback)
    .setDefaultCallback(ProgressCallback.class)
    .build();

    代码混淆

    -dontwarn com.kingja.loadsir.**
    -keep class com.kingja.loadsir.** {*;}


    代码下载  :LoadSir-master .zip

    原文链接  :https://github.com/KingJA/LoadSir

    收起阅读 »

    Android智能下拉刷新框架-SmartRefreshLayout

    Android智能下拉刷新框架-SmartRefreshLayoutSmartRefreshLayout以打造一个强大,稳定,成熟的下拉刷新框架为目标,并集成各种的炫酷、多样、实用、美观的Header和Footer。 正如名字所说,SmartRefreshLa...
    继续阅读 »

    Android智能下拉刷新框架-SmartRefreshLayout

    SmartRefreshLayout以打造一个强大,稳定,成熟的下拉刷新框架为目标,并集成各种的炫酷、多样、实用、美观的Header和Footer。 正如名字所说,SmartRefreshLayout是一个“聪明”或者“智能”的下拉刷新布局,由于它的“智能”,它不只是支持所有的View,还支持多层嵌套的视图结构。 它继承自ViewGroup 而不是FrameLayout或LinearLayout,提高了性能。 也吸取了现在流行的各种刷新布局的优点,还集成了各种炫酷的 Header 和 Footer。

    特点功能:

    • 支持多点触摸
    • 支持淘宝二楼和二级刷新
    • 支持嵌套多层的视图结构 Layout (LinearLayout,FrameLayout...)
    • 支持所有的 View(AbsListView、RecyclerView、WebView....View)
    • 支持自定义并且已经集成了很多炫酷的 Header 和 Footer.
    • 支持和 ListView 的无缝同步滚动 和 CoordinatorLayout 的嵌套滚动 .
    • 支持自动刷新、自动上拉加载(自动检测列表惯性滚动到底部,而不用手动上拉).
    • 支持自定义回弹动画的插值器,实现各种炫酷的动画效果.
    • 支持设置主题来适配任何场景的 App,不会出现炫酷但很尴尬的情况.
    • 支持设多种滑动方式:平移、拉伸、背后固定、顶层固定、全屏
    • 支持所有可滚动视图的越界回弹
    • 支持 Header 和 Footer 交换混用
    • 支持 AndroidX
    • 支持横向刷新

    智能之处

        智能是什么?有什么用?

    智能主要体现 SmartRefreshLayout 对未知布局的自动识别上,这样可以让我们更高效的实现我们所需的功能,也可以实现一些非寻常的功能。 下面通过自定义Header 和 嵌套Layout作为内容 来了解 SmartRefreshLayout 的智能之处。

    自定义Header

    我们来看这一下这个伪代码例子:

        <SmartRefreshLayout>
    <ClassicsHeader/>
    <TextView/>
    <ClassicsFooter/>
    SmartRefreshLayout>

    在Android Studio 中的预览效果图

    e479f09949e46ba9e92c4c3c39d4925f.jpg

    对比代码和我们预想的一样,那我们来对代码做一些改动,ClassicsHeader换成一个简单的TextView,看看会发生什么?

        <SmartRefreshLayout>
    <TextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:gravity="center"
    android:background="#444"
    android:textColor="#fff"
    android:text="看看我会不会变成Header"/>
    <TextView/>
    <ClassicsFooter/>
    SmartRefreshLayout>

    在Android Studio 中的预览效果图 和 运行效果图

    2d3eee1d2fcf024d213fe12f969e56a7.jpgdd72f12c3d967b05187634f49320a359.gif

     

    这时发现我们我们替换的 TextView 自动就变成了Header,只是它还不会动。要动起来?那么太简单啦,网上随便一搜索就一大堆的 gif 。

    我们选择 环游东京30天:GIF版旅行指南中的这张:

    2afef925fdfb303b1c9b0b46e33eca42.gif

    接着我们来改代码:

    compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.3'//一个开源gif控件
        <SmartRefreshLayout xmlns:app="http://schemas.android.com/apk/res-auto"
    app:srlDragRate="0.7"
    app:srlHeaderMaxDragRate="1.3">
    <pl.droidsonroids.gif.GifImageView
    android:layout_width="match_parent"
    android:layout_height="150dp"
    android:scaleType="centerCrop"
    android:src="@mipmap/gif_header_repast"
    app:layout_srlSpinnerStyle="Scale"
    app:layout_srlBackgroundColor="@android:color/transparent"/>
    <ListView/>
    <ClassicsFooter/>
    SmartRefreshLayout>

    在 Android Studio 中的预览效果图 和 运行效果图

     bbd8c7e3d34906f83eaab80fc602019b.jpg8cd5e90aa62ce2b2f449fa4502b42f25.gif


    哈哈!一行Java代码都不用写,就完成了一个自定义的Header

    嵌套Layout作为内容

    如果boss要求在列表的前面固定一个广告条怎么办?这好办呀,一般我们会开开心心的下下这样的代码:

    <LinearLayout
    android:orientation="vertical">
    <TextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:gravity="center"
           
    android:text=
    "我就是boss要求加上的广告条啦"/>
    <SmartRefreshLayout>
    <ListView/>
    SmartRefreshLayout>
    LinearLayout>

    但是在运行下拉刷新的时候,我们发现 Header是在广告条之下的,看着会别扭~,其实我们可以试试另一种方式,把广告条写到 RefreshLayout内部,看看会发生什么?

    <SmartRefreshLayout>
    <LinearLayout
    android:orientation="vertical">
    <TextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:gravity="center"
    android:text="我就是boos要求加上的广告条啦"/>
    <ListView/>
    LinearLayout>
    SmartRefreshLayout>

    由于伪代码过于简单,而且运行效果过于丑陋,这里还是贴出在实际项目中的实际情况吧~

     c0057fa29a0efd57f10ce15e0710c412.gif

    我们注意看右边的图,仔细观察手指触摸的位置和下拉效果。可以看到在列表已经滚动到中部时,轻微下拉列表是不会触发刷新的,但是如果是触摸固定的布局,则可以触发下拉。从这里可以看出 SmartRefreshLayout 对滚动边界的判断是动态的,智能的!当然如果 SmartRefreshLayout 的智能还是不能满足你,可以通过 setListener 自己实现滚动边界的判断,更为准确!


    代码下载: SmartRefreshLayout-master.zip

    原文链接: https://github.com/scwang90/SmartRefreshLayout

    收起阅读 »

    人脸识别圆形预览效果

    FaceCircleView-master.zip原文链接:https://github.com/yangcoder1/FaceCircleView

    【评论有礼】看直播,拿好礼!社交App出海技术指南:全球消息网络互联及出海最佳实践

    中国移动互联网企业出海的春天来了!!!如果,我想做海外社交,还有机会吗?如何从0到1搭建一款社交产品?当新生代社交演化为线上沉浸式虚拟化社交,虚拟角色+沉浸式社交互动场景成为行业标配,如何保障这些新场景新玩法的流畅使用,对于技术服务商的稳定、安全、服务等又有哪...
    继续阅读 »

    中国移动互联网企业出海的春天来了!!!
    如果,我想做海外社交,还有机会吗?如何从0到1搭建一款社交产品?
    当新生代社交演化为线上沉浸式虚拟化社交,虚拟角色+沉浸式社交互动场景成为行业标配,如何保障这些新场景新玩法的流畅使用,对于技术服务商的稳定、安全、服务等又有哪些要求?
    3月25日 19:00-20:00邀您观看 【环信X快出海】 第51期线上公开课《社交App出海技术指南:全球消息网络互联及出海最佳实践》,一起解锁出海新生代明星社交APP新玩法。锁定环信公开课直播间,看直播就能抽取惊喜好礼!

    直播福利1:直播间互动,好礼送不停
    直播过程中参与直播聊天室互动问答,就可抽取惊喜礼物。

    冬冬微信_副本.jpg

    (扫我加入社群)
    温馨提示:本次活动所有奖励必须加入社群收看直播领取,否则无效。

    直播福利2:直播社群微信红包抽奖
    本次直播社群会有3位幸运的小伙伴获得惊喜礼品,直播过程中,社群会同步进行三轮红包抽奖,手气最佳者获得礼品。

    冬冬微信_副本.jpg

    (扫我加入社群)
    温馨提示:本次活动所有奖励必须加入社群收看直播领取,否则无效。

    直播福利3:环信社区提问,赢定制好礼
    参与本贴进行提问,分享嘉宾会抽取3个问题,由嘉宾进行回答,并送出奖品。

    论坛奖品图片.png

    冬冬微信_副本.jpg

    (扫我加入社群)
    温馨提示:本次活动所有奖励必须加入社群收看直播领取,否则无效。

    带活码-海报.jpg


    收起阅读 »

    视频数据流

    @interface ViewController () @end @implementation ViewController - (void)viewDidLoad {     [super viewDidLoad];     // Do any a...
    继续阅读 »
    @interface ViewController ()

    @end

    @implementation ViewController

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
        button.frame=CGRectMake(0, 0, 200, 50);
        [button setTitle:@"aa" forState:UIControlStateNormal];
        [button addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }

    -(void)click:(UIButton *)btn{
       
        UIImagePickerController *imagePicker=[[UIImagePickerController alloc] init];
        imagePicker.delegate=self;
    //    imagePicker.view.frame=s
        if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]){
         imagePicker.sourceType=UIImagePickerControllerSourceTypeCamera;
           
        }
       // imagePicker.allowsEditing=YES;
    //    [self.view addSubview:imagePicker.view];
    [self presentViewController:imagePicker animated:YES completion:^{
       
    }];
    }


    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo {
       
    }
    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
        [self dismissViewControllerAnimated:YES completion:nil];
        NSLog(@"%@",info);
      UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage];

        self.image.image=image;
    }
    - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{
        [self dismissViewControllerAnimated:YES completion:^{
           
        }];
    } 收起阅读 »

    【直播回顾】华为云×环信,强强联手实现用户增长,降本增效加快企业转型!

    1月28日晚上19:00,华为云云市场新生态直播邀请到环信产品总监王璨老师,以《浅谈增长秘籍:看APP如何利用IM云拉新促活》为题做了分享,他提到互联网的特点是以用户为中心,企业要靠不断提升客户体验,帮助客户“定义”产品,累积形成用户规模优势,从而形成突破性增...
    继续阅读 »
    1月28日晚上19:00,华为云云市场新生态直播邀请到环信产品总监王璨老师,以《浅谈增长秘籍:看APP如何利用IM云拉新促活》为题做了分享,他提到互联网的特点是以用户为中心,企业要靠不断提升客户体验,帮助客户“定义”产品,累积形成用户规模优势,从而形成突破性增长。同时结合环信即时通讯云和客服云详细讲解了用户增长、利用IM做拉新促活、降本增效的具体方法及策略,也分享了环信联合华为云助力企业数字化转型的成功实践,现在让我们来一起回顾一下要点吧!

    f449e79bfab08b16b5e46893046090b.jpg


    王璨总监在直播中谈到,环信一共有两条主要产品线——即时通讯云及客服云,在用户增长方面,环信结合产品给出了有竞争力的解决方案。
    首先是关于APP增长之分析。当前企业营销运营环节中遇到的突出问题包括流量红利褪去,公域流量获取成本高等,疫情之下,企业加快了数字转型,需要严格控制投入产出比,所以如何获取私域流量成了众多企业能否良好生存及发展的关键因素。
    结合当前微信生态,微信私域流量的红利也终将褪去,企业触达用户的渠道更加分散,针对此,环信推出即时通讯云帮助客户打造全渠道的私域流量运营体系,在用户获取方面使用5G富媒体消息、邮件、外呼机器人等工具主动触达,利用微信公众号、小程序、客服等进行公域流量转化,获取用户后,及时与用户建立连接并激活。
    在智能客服助力企业降本增效方面,环信推出智能客服机器人和外呼机器人,极大的节省了人力成本,提升客服效率。
    根据目前产品,王璨老师也深入分析了环信是如何利用IM云拉新促活,使用客服云降本增效。
    环信即时通讯云(IM)将资源部署在华为云上,利用多集群的方案,为客户提供更稳定、更高等级的服务,基于国内开发者客户需求,提供深度优化的SDK。同时环信即时通讯云产品提供包括直播、视频会议、语音连麦聊天室、小程序、小游戏、企业内部IM、智能硬件等多种解决方案。
    环信客服云(CEC)提供全渠道互动与全媒体接入,来自不同媒体的服务请求均可以统一接入,一键回复。帮助用户打造跨网、跨界、跨平台的极致客户体验。
    环信智能问答机器人为用户降低人工成本,通过大量数据消息训练机器人,优化参数,最终提升工作效率;外呼机器人系统结合云呼叫中心,呼出频率远高于人工;同时环信客服云提供视频客服,全平台支持、灵活接入,极大提升用户使用体验;统一融合智慧工单,不仅能实现工单的有效整合和高效流转,更能快速发现产品功能缺失,提升内部运营效率;提供开放平台能力,可以通过自由组合实现客户的个性化客服需求。
    环信即时通讯云及客服云产品通过了华为云相关安全认证,将服务部署在华为云上,稳定性和技术支持能够得到保障,多年来与众多行业头部企业合作,在跨境电商、在线教育、医疗、制造、金融、智能硬件等行业都有成功实践!
    获取更多详情请点击https://0x7.me/NaqQm,进入华为云云市场直播间观看精彩回放! 收起阅读 »

    【华为云生态直播】浅谈APP增长秘籍,看直播抽取华为手环、蓝牙音箱

    5G时代,企业如何提升用户体验、实现增长?众所周知IM是APP拉新促活的标配服务,环信作为全球最大的即时通讯云服务商如何做到亿级高并发助力全民直播获客?小程序IM如何赋能APP客户?如何在全球200多个国家和地区服务社交、游戏等出海企业?以及面对互联网新规,如...
    继续阅读 »
    5G时代,企业如何提升用户体验、实现增长?众所周知IM是APP拉新促活的标配服务,环信作为全球最大的即时通讯云服务商如何做到亿级高并发助力全民直播获客?小程序IM如何赋能APP客户?如何在全球200多个国家和地区服务社交、游戏等出海企业?以及面对互联网新规,如何提供敏感词过滤、智能反垃圾、鉴黄、图片合规等服务,让APP远离不良信息侵扰。

    同样,作为领先的全渠道智能在线客服,环信如何为企业降低60%客服成本,提高80%线索转化?看直播,抽取华为手环、蓝牙音箱、体脂秤、双肩包等惊喜礼物,1月28日晚7点,华为云云市场新生态直播邀您不见不散!


    带二维码_副本.jpg


      收起阅读 »

    程序员如何写出高质量的代码程序

    编码是程序员最重要的工作,每个程序员都希望自己可以写出优雅,高性能,高质量的代码,对于大师级别的程序员,他们的写的代码就和艺术品一样,你会忍不住发出惊叹,他们怎么可以创造出如此惊艳的作品出来。 下面笔者就以自己的浅薄学识和一些经验来总结下优秀的程序应该具有的特...
    继续阅读 »
    编码是程序员最重要的工作,每个程序员都希望自己可以写出优雅,高性能,高质量的代码,对于大师级别的程序员,他们的写的代码就和艺术品一样,你会忍不住发出惊叹,他们怎么可以创造出如此惊艳的作品出来。
    下面笔者就以自己的浅薄学识和一些经验来总结下优秀的程序应该具有的特点。

    每个变量的命名都深思熟虑

    普通程序员的变量命名很随便,以至于随便到abcd都会出来,而高质量的代码的命名则很规范,既不长,也不短,既可以读出它们的含义,又不至于显得啰嗦,总之,从变量命名你就能读出一个程序是否优雅。

    从配置文件中读取变量

    很多人喜欢在程序中通过注释来修改变量值,这样的做法非常不对,首先不说无用地注释影响了代码的整洁,就通过修改代码来修改变量的值就是不优雅的。

    一个优秀的程序,一定是从配置文件中读取所需要的变量的,而修改配置文件对于一个人来说远远比去源代码中修改变量值要方便的多得多。

    当你学会从配置文件中读取配置,修改配置的时候,你的程序才是优秀的。

    一定要有测试代码

    一个高质量的程序一定会有测试代码,记住无论程序功能多么简单,我们都要写测试代码。为什么TDD会流行,因为很多人懒得写代码,而TDD就是强迫你写测试代码,因为这样可以让代码更加健壮,同时,其它人修改代码也可以不会造成更重大影响。

    我们不一定使用TDD进行程序开发,但是一定要写测试代码,有了测试代码,你的程序才经得起折腾,记住,有时候你会犯迷糊,但是测试代码不会,跑通过测试用例的代码至少可以让你减少很多错误。

    一定要写日志

    一个程序开发之后,你是没有办法预测它的使用环境和使用方式的,你能做的就是在它出现错误的时候记录下日志,这样你才可能进行分析。同时,在程序开发的过程中,通过记录日志也可以方便我们进行代码的调试,日志也是调试分析的一种方式。

    永远不要重复写代码

    古人云事不过三,写代码也一样,当你在很多地方写了重复代码的时候,你要记得将它们重构,永远不要写重复的代码,发现重复的时候,记得使用函数将它抽象出来。

    很多人喜欢拷贝代码,然后你会发现他的程序中好多代码是一样的,而当他要修改代码的时候,不得不每一处都需要修改,这不仅浪费时间,还可能造成代码的遗漏。

    代码格式要统一

    记得以前听过一个笑话,我们中国人写的代码,一个人写的像一千个人写的一样,而印度人写的代码,一千人像一个人写的一样。

    我们不要求所有人写的代码风格都一模一样,但是我们需要你写的代码前后要统一,同时要遵循代码推荐分隔。

    现在所有的语言都有自己的代码格式风格,你只要按照规则来写就好。

    总结

    优秀的代码每一个变量的命名都是反复斟酌的,每一个函数都是力求最精简的,每一个方法都是尽力是最高效的。

    自己写完的代码一定要复审,有时候很多明显的错误一定要避免。

    代码之道永无止境,我们只有不断地总结,才能写出接近优秀的程序,而优秀的程序永远都不会存在。 收起阅读 »

    环信大学:2021漫游指南,PC Web、Uni-App、小程序集成环信IM都在这里了

    与其说这是一篇集成攻略,其实这更多是对于官网文档的一篇解释说明,相信很多的小伙伴在准备将环信的IM即时通讯能力集成或移植在自己的项目上都会出现一头雾水或无从下手的感觉,即使看遍了官方文档也不免心生疑问: “啥?!啥?!啥?!这是啥?!文档写的是个啥?!” ...
    继续阅读 »
    与其说这是一篇集成攻略,其实这更多是对于官网文档的一篇解释说明,相信很多的小伙伴在准备将环信的IM即时通讯能力集成或移植在自己的项目上都会出现一头雾水或无从下手的感觉,即使看遍了官方文档也不免心生疑问:
    “啥?!啥?!啥?!这是啥?!文档写的是个啥?!”

    图片1.jpg


    所以我决定更加直白且详细的描述一下如何集成web端的IM SDK,(小程序、Uni-app通用)以下是概览:
    一、 注册环信console,创建自己的应用,拿到Appkey。
    二、 粗略浏览官方文档,熟悉大致流程及接口。
    三、 Clone Demo,挑出必选引入文件。
    四、 Hello World! (从注册登陆开始)。
    常见问题以及error报错查询入口:
    http://docs-im.easemob.com/faq
    http://docs-im.easemob.com/im/other/errorcode/web
    http://docs-im.easemob.com/im/other/errorcode/restapi
    以下为较为详细的描述一些集成步骤,如一些步骤已非常了解且已操作完成请自行跳过。
    一、注册环信console,创建自己的应用,拿到Appkey
    PS:这一步骤,各端通用。
    1. 注册console 管理后台
    相信经常使用第三方服务的小伙伴都非常熟悉这一步(不熟悉?没关系,照着葫芦画瓢就行),打开开发者平台进行注册登录创建等操作,这一点我就不过多赘述,直接上注册平台链接:https://console.easemob.com/user/login

    图片2.png


    注册成功之后会跳转到这样的一个界面:

    图片3.jpg


    这个就是console后台管理页面,这个实际就属于一个后台管理系统,创建自己的项目应用,监测日活,统计注册用户数,常见的一些增删改查操作都可以执行,没事可以捣鼓捣鼓看看有哪些功能,这里我就不多介绍了,咱们接着往下进行。
    2. 创建应用
    创建应用这一步搁咱们JavaScript其实可以理解为 new 了一个空对象,var了一个空变量等等随意怎么比喻都可以。如果放在现实当中其实更像是创建了一个公司,这个公司有公司名也就是Appname,而每个公司肯定有一个唯一的公司代码(公司的身份证ID号),也就是Appkey。公司人数也就是创建出来的应用下注册的用户数量等等。
    点击添加应用开始创建:

    图片4.png


    创建应用的详情页:

    图片5.png


    这里我特意框住是因为经常有人告诉我:“创建出来的appkey在客户端测试时候点击注册没法注册,报错描述是401无权限是怎么回事?”
    最后发现是创建应用的时候注册模式选的是授权注册,你选个授权注册,看眼描述就知道这种注册模式相当于注册是要经过审批的,如果测试阶段可以使用开放注册,后期还可以修改注册模式。
    这个是创建成功之后点击进入应用的基本信息描述,分别对应的是什么功能也有描述,Appkey也就成功拿到了,由orgname +“#”+appname组成的appkey。如图:

    图片6.png


    这里特别说下这个Client ID和Client Secret ,有了Client ID和Client Secret就可以获取到管理员token从而可以在应用下随意增删改查,发消息,之前有遇到泄露导致客户端用户莫名其妙收到一些骚扰信息,当然这都算轻的,所以正式环境下一定要保管好Client ID和Client Secret !!!
    二、粗略浏览官方文档,熟悉大致流程及接口
    PS:这一步骤,各端通用
    可能有的小伙伴觉得这一步我再拉出来讲意义不是很大没什么用,其实不然,因为在协助一些小伙伴集成SDK中发现有的朋友对于在自己项目中要用到的接口都还不知道在哪,更谈不上如何使用。文档通篇一字不落看下来也不太现实,但是大致知道一些后期咱们的项目中有哪些接口可能会用到,知道大概在哪能找到调用方式还是很有必要的。
    一篇文档中我们可以从这些方向看:

    图片7.png


    SDK功能分类,更新日志(了解SDK更新周期,新增功能,修复问题),Demo的使用介绍,兼容性以及常见问题。
    Uni-app打包双端的介绍,运行平台描述,源码的GitHub地址。

    图片8.png


    而且文档中也有描述一些可能会遇到的坑,一些解决方案。例如:发送URL类型的图片消息需要在config配置文件中配置。

    图片9.png



    图片10.png


    通过“WebIM.conn.mr_cache =  ”方法可以重置拉取历史消息的游标。

    图片11.png


    常用的新消息提示如何去处理,等等都会有一些描述,磨刀不误砍柴工,因此我强烈建议各位在正式开始集成之前首先浏览一下咱们对应端的文档。
    在此我也正好有几个常用的功能场景提供给大家,文档中有介绍,为了方便各位也还是直接上链接。
    消息回执(涉及到web端已读未读的处理):
    http://docs-im.easemob.com/im/web/basics/message#%E6%B6%88%E6%81%AF%E5%9B%9E%E6%89%A7
    消息漫游(涉及到web端拉取历史SDK接口):
    http://docs-im.easemob.com/im/web/basics/message#%E6%B6%88%E6%81%AF%E6%BC%AB%E6%B8%B8
    昵称与头像(涉及到昵称与头像的处理):
    http://docs-im.easemob.com/im/other/integrationcases/nickname
    获取管理员token(一些服务端rest接口的调用需获取管理员token):
    http://docs-im.easemob.com/im/server/ready/user#%E8%8E%B7%E5%8F%96%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90
    拉取历史消息文件(将聊天产生的历史消息保存到本地服务器):
    http://docs-im.easemob.com/im/server/basics/chatrecord#%E8%8E%B7%E5%8F%96%E5%8E%86%E5%8F%B2%E6%B6%88%E6%81%AF%E6%96%87%E4%BB%B6
    更多常用实现场景可以参考文档常见方案一栏,里面可能有些只有IOS 和 Android 解决方案,但是web端实际也是相似的处理方式,瞜一眼相信会有些启发。

    图片12.png


    http://docs-im.easemob.com/im/other/integrationcases/live-chatroom
    文档已浏览完毕,接着往下进行。
    三、Clone Demo、挑选出必要文件
    其实这一步主要面向于直接下载了官网Demo,但是不知道如何把将Demo往里引入的同学,在协助集成的过程中,我经常遇到有些朋友启动Demo测试完成之后,直接就把所有Demo文件拷贝到了原有项目当中,我们知道前端的部署打包的性能优化是与压缩精简代码,减少请求数量,页面结构,浏览器缓存等因素是相关的,我并不建议完全将Demo拷贝到咱们的原项目当中。
    首先我想说下现有PCweb-Demo uni-appDemo 微信小程序Demo等Demo 的意义是为我们开发者小伙伴展示功能接口的实际运用,即时通讯能力的接口测验,各接口组合使用的运用逻辑及调用顺序 ,当然更是给了咱们在实际项目开发当中有了一定的参考作用。由于其功能性的考虑并没有按照线上产品的标准去做,所以其代码内部接口中有些接口的调用以及请求并未考虑性能因素,功能并未完全实现展示。
    相信通过我的描述,也就明白了为什么不建议将Demo完全拷贝到原有项目中,因为可能会对咱们的性能优化造成一定的影响,并且可能会有很多代码对项目中的功能实现并无任何帮助,有些组件关联,运行可能会有更多难以排查的报错,更多是建议可以参考一下Demo当中的写法。
    下面我先介绍一下uni-app-Demo集成 IMSDK的操作:
    Uni-App-Demo GitHub地址:
    https://github.com/easemob/webim-uniapp-demo
    1. 将clone下来Demo于HbuilderX中打开,首先咱们不急着运行先了解一下目录下各个文件的作用。


    图片13.png


    2. 将核心文件导入到自己的测试项目或正式项目中。
    随着环信3.x IM SDK的更新,现在引入依赖文件相对之前2.x IM SDK的集成引数个文件无疑是简单了许多。
    Uni-app-Demo中需要引用的文件截图:

    图片14.png


    其实除SDK必须引入之外,WebIMConfig.js 以及WebIM.js也可以不引用,但是在项目中必须要存在,听起来很矛盾,但实际含义就是WebIMConfig.js该有的配置你必须要配置,WebIM.js中的初始化这一步你也必须要进行初始化,至于你要命名为其他文件名,放在其他位置也不是不可以的。
    SDK包:PCWeb端:websdk3.x.js 小程序/Uni-app:wxsdk3.x.js
    WebIMConfig.js:这个文件的作用就是将引入的WebIMSDK进行自定义配置:

    图片15.png


    如图文档上有对各个配置项的作用详细介绍,建议在集成过程中的配置页可以参考Demo中的配置进行配置,但是各个配置项的开启与否,参数设置请自行按需设置。
    依然是在协助小伙伴集成环信IM的过程经常会遇到有小伙伴问配置项中的:“socket Server地址:socketServer: '//im-api-v2.easemob.com/ws',    // ”和“rest Server地址:restServer: '//a1.easemob.com' ”,“需不需要配置成自己项目地址,或者自己去定义这两个参数?”,对于这个问题,我只能回答说千万别!这个是定义好了的,你要做的基本就只需要将appkey:改成自己申请的,其他几乎不用动。
    WebIM.js:将SDK的配置以及引入的SDK进一步做处理简称:初始化。
    导出SDK 并引入WebIMConfig.js文件,将SDK重命名为WebIM挂载到window或uni或wx下,视情况决定。

    图片16.png


    同样这一步文档也有详细的描述:

    图片17.png


    3. 完成文件导入挂载,开始配置并添加监听回调。
    如图,监听仅截图一部分:

    图片18.jpg


    在集成Web端(包括小程序以及Uni-app)IM中,所有的事件监听都是要靠WebIM.conn.listen( ),由此可以看出其重要性。
    所以在初始化阶段结束后我们首要将监听配置添加上。
    至于监听的具体位置应该添加在哪里这个实际并没有严格的要求,PCwebDemo中是将监听写在了WebIM.js中,微信小程序Demo是将监听写在了app.js,uni-app是将监听写在了App.vue根组件中,多数是写在了全局。但其实监听也可以写在局部,前提局部能够访问到WebIM,然后在局部添加上监听回调,不管是全局监听还是局部监听都要在SDK接口调用之前布置好监听并开启监听。
    下面我们再来聊一下监听中的个别监听回调的作用以及实际运用,其监听的作用文档已有详细描写且较为好了解,这里就不再一一赘述。
    onOpened: function ( ) {},         //连接成功回调 
    此监听为与环信连接成功之后触发的监听回调,经常会出现一个问题就是明明环信conn.open登陆接口都已经返回success都已经返回token了,可是发送消息依然报错,接收消息也接收不到。那么我们首要检查的就是该监听有没有触发,放个console.log( )看下登陆之后的该回调有没有成功的打印,如果打印了那么说明成功与环信建立了连接!也就可以进行其他的操作了。
    经常有朋友问:“登陆成功之后打印message形参为什么是undefined?是不是登陆有问题?”,其实不是这个message并不会有任何返回,登陆是否成功一切以onOpened监听回调是否执行为准!

    图片19.png


    在该监听的实际运用中Demo中其实也做了很好的演示,就是登陆之后的页面跳转,这个非常常用,因为IM聊天功能是否可以使用首要的判断必然是与环信是否建立连接成功,成功那么跳转进入聊天页面。
    onClosed: function ( message ) {},        //连接关闭回调
    此监听为与环信连接关闭的监听回调,其实也没有具体要描述的地方,更多的是该监听经常用来处理在与环信断开连接时候的一些UI层面的提示,或者再意外断开连接之后退回到login页面,以及有些场景下进行手动重连。
    onMessage:function ( message ) {},   //监听收到消息的回调
    这里的onMessage是对所有收到消息的回调的一个简称,例如:onTextMessage 监听文本消息、onPictureMessage 监听图片消息、onAudioMessage 监听音频消息,等等... 这个监听相信不用过多描述就可以清楚其功能,消息监听回调非常的核心,所有的消息都是仰仗这几个监听回调拿到的。测试的时候可以在监听回调中定义形参并控制台打印参数查看消息监听是否正常触发并接收数据。
    截图为onTextMessage监听收到的单聊消息

    图片20.png


    onError: function ( message ) {},          //失败回调
    此监听回调可监听SDK中大多数的error抛出,在集成开发过程中帮助更快定位问题原因,并且可在回调中根据error信息从UI层面进行提示,因此其实际运用相对也常用。
    截图为Uni-appDemo利用onError监听做出的提示以及一些操作。

    图片21.png


    当然有些error出现之后大家可能不知道其具体含义下面为分析一下error异常的判断思路,
    首先onError监听回调的message字段就已经给出了大致error原因:
    例图中onError回调返回的对象中的message字段value的值中给出了“login failed”登陆异常,接着我们看data字段下给出的具体原因,不难看出invalid password 不打开百度翻译也能猜出密码有问题,那么我们也就可以以此判断,登陆密码错误,或者对用户做出弹窗提示。

    图片22.png


    同样的type:1,同样的message:“login failed”,error详情就又不一样了,error介绍:“user not found” 用户找不到,所以具体的判断error原因还是要以data下的error_description给出的原因去定位。

    图片23.png


    当然有些error也会出现一些其他的情况,并不返回message 参数,data也是空的,只给出了一个type类型,像这种情况其实就是在没有登陆的情况下调用了SDK接口,那么这时首先要判断的就是是否登录了环信,或者已经登陆但是又断开了与环信的连接。
    截图为未登录调用了获取群组详情返回的error。

    图片24.jpg


    在已登录的情况下查询群组详情,error详情直接给出了找不到 group ID为XXXXXX的群组,此时就可以以此判断,或给出提示查找不到群ID为...的详情

    图片25.png


    onPresence: function ( message ) {},       //处理“广播”或“发布-订阅”消息,如联系人订阅请求、处理群组、聊天室被踢解散等消息。
    此监听正如描述所讲,处理“广播”或“发布-订阅消息”,至于发布-订阅消息可以去了解一下设计模式,发布订阅者模式,这里就不再进行解释了,这里主要讲的是该监听如果在实际运用中的作用是什么。
    我们先看下Uni-appDemo中的示例:

    图片26.png


    通过观察我们看到了示例中运用到了switch case 语句通过回调里message中不同的type类型从而对应执行不同的提示或其他逻辑操作。
    在实际SDK集成过程中,例如:“申请添加好友、删除好友,邀请入群,他人退群,新增群组管理员、他人加入聊天室,退出聊天室”等操作均会触发此监听的执行并回调出对应行为类型的type字段,根据type类型我们从而可以给出对应的UI层面的提示,或发布-订阅事件,uni-appDemo中的disp.fire(‘XXXX’),正是发布-订阅模式中的发布事件。
    如果我们需要详细的类型,则可以在文档中的事件监听找到:
    好友状态事件监听:
    现有的好友事件监听,已新增对应的回调监听:

    图片27.png


    群组事件监听:
    文档地址:
    http://docs-im.easemob.com/im/web/basics/group#%E7%BE%A4%E7%BB%84%E4%BA%8B%E4%BB%B6%E7%9B%91%E5%90%AC

    图片28.png


    聊天室事件监听:
    文档地址:
    http://docs-im.easemob.com/im/web/basics/chatroom#%E8%81%8A%E5%A4%A9%E5%AE%A4%E4%BA%8B%E4%BB%B6%E7%9B%91%E5%90%AC


    图片29.png



    以上变为常用的几个监听的介绍,具体的使用还是以咱们实际项目中的逻辑为准。
    SDK,config配置,以及初始化,监听都已配置完毕,下面我们就开始注册、登陆环信ID,发出我们的第一条消息吧!
    四、Hello World ! (从注册登录开始)
    环信所有的功能性SDK接口调用都是要从注册及登陆成功方可有效使用,如已注册,登陆则是客户端调用的第一个接口。
    下面我将从注册接口开始讲起:
    注册,web端注册接口如图:

    图片30.png


    客户端是否能够直接调用注册接口,需要先去环信IM管理后台检查该Appkey的注册模式是否为开放注册,授权模式的话,此接口调用注册会直接出现下图情况

    图片31.png


    Open registration doesn't allow, so register user need token
    开放注册不允许,所以注册用户需要token
    注册成功会触发注册接口中的success回调,并有注册信息的返回如图:返回字段包含创建时间,appname,orgname等信息。

    图片32.png


    注册完成之后便可以开始调用登陆接口完成登录操作。
    下面我们来了解SDK提供的两种登陆方式:
    1. 用户名密码登陆(环信ID、密码登陆),截图为文档——登陆接口:

    图片33.png


    user: String //环信ID
    pwd:String //注册时的密码
    appKey  // appkey 初始化后appkey
    2. token登陆(使用token进行免密登陆)

    图片34.png


    该登陆方式可以进行免密登陆,常见用于刷新页面之后的二次登陆。
    比较常用到的场景,例如从login页面跳转到chat页面,用户在当前页面刷新之后不再进行login页面登陆直接正常进行聊天。(刷新页面是会断开与环信的连接,如不登陆则无法调用SDK接口)。
    token获取:token的获取是在用户名密码登陆成功后的success回调中返回获取。

    图片35.png


    access_token字段为token。
    expires_in字段为token有效期,单位为秒,有效期内token可重复使用。
    uni-app-demo中的用户名密码登陆的接口调用

    图片36.png


    Uni-app-demo中的token登陆:

    图片37.png


    有时我们会遇到success成功的返回了token,可是发送消息或者调用其他接口还是会出现报错,或者提示未登录的error,这时我们首要排查的情况还是检查一下onOpened监听是否成功触发,与环信的连接建立还是要以onOpened监听触发为准!

    图片38.png



    图片39.png


    监听成功打印,意味着登陆成功!
    这里顺便给大家两个检验是否登录的方法。
    1. WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登陆:

    图片40.png


    2. WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录:

    图片41.png


    到这里登陆就说完了,至于为什么没有讲下去也是因为只要登录成功,其他接口就可以按需调用,相信以各位小伙伴的能力接着往下进行也不是很困难了,当然也说不定有些奇葩问题搞不定,那么建议咱们可以去环信官网去寻求我们支持同事的帮助,将遇到的问题描述清楚,问题将在24H以内得到回复。

    图片42.png



    图片43.png


    最后愿JavaScript之父布兰登·艾克
    保佑各位项目顺利上线! 收起阅读 »

    双旦有“礼”狂欢季

    今天有收到圣诞礼物吗? 如果没有,也没关系哦。 因为这次双旦节, 我们给大家准备了非常丰厚的礼品, 快来扫码参加活动吧!  
    今天有收到圣诞礼物吗?
    如果没有,也没关系哦。
    因为这次双旦节,
    我们给大家准备了非常丰厚的礼品,
    快来扫码参加活动吧!

    微信图片_20201225105015.jpg


     

    公开课 | 移动开发助力在线教育增长实战

    2020年,线上教育大火!受用户需求的影响,学校、教育培训机构都把线下的课程搬到线上,在线教育行业随之开启一场“新基建”。 然而,这是一项巨大且缓慢的工程,线上教学平台的开发、营销获客方法的探索、运营工具的管理、教学内容的研发……教育行业究竟怎样实现线上和线...
    继续阅读 »
    2020年,线上教育大火!受用户需求的影响,学校、教育培训机构都把线下的课程搬到线上,在线教育行业随之开启一场“新基建”。

    然而,这是一项巨大且缓慢的工程,线上教学平台的开发、营销获客方法的探索、运营工具的管理、教学内容的研发……教育行业究竟怎样实现线上和线下的融合?教育OMO(Online-Merge-Offline)模式又该怎么跑通?教育增长到底该如何实现?

    环信联合MobTech袤博及保利威,结合当前教育行业的最新趋势,给出一系列全新产品和技术解决方案,全方位助力教育行业平滑转型、降本增效。

    干货1:从在线教育app全生命周期精准分析运营之道

    干货2:通过稳定安全的PaaS+SaaS+云服务,打造教学、管理、营销一体化解决方案

    干货3:通过直播平台助力教育机构建立私域流量

    干货4:资深在线教育专家独家分享爆发式增长秘诀


    更有精美礼品与价值万元的充电大礼包等你来领!

    如果你还在为在线教育的运营突破口而困扰

    那就抓紧报名参与!一起来碰撞出灵感的火花吧!
     
    直播观看链接:https://live.polyv.cn/watch/2047367 


    1.jpg



    2.jpg



    环信冬冬_副本.jpg


    添加小助手微信【huanxin-hh】进入活动直播群 收起阅读 »