管理ButterKnife源码分析

前言

在N久此前,自从实验室里面的学长推荐自家用butterknife后,
从此的类型再也离不开butterknife了,不过自以为对它很熟时,前不久博客园实习生招聘二面却被面试官洗刷了一顿。然后所有二面完全是被虐的感觉到,估摸最终会挂,哎!
当时被问到butterknife的兑现,懵逼的自身想都不想就答上了诠释加反射。但是面试官却一脸猜疑的问我:你规定?除了反射还有其他艺术么????
自我了个去,难道butterknife不是用的反光??难道还有任何方法来兑现这东西儿么?不行,面试完了迅速clone
源码下来看看。不看不驾驭,一看吓一跳,原来还真不是用的笺注加反射。在此感谢面试官为自己打开了新世界的大门,原来申明仍是可以这么用!

设计师或许是这几个世界上最亟需灵感的人,大家涉猎的知识五花八门,每日的读书量无穷无尽,没事就右键另存张图片几乎就是一种习惯,无意之中存个十万八万张图片也是正规景况…

Butterknife用法

自家信任学过android开发相应大概都用过Butterknife啊,就算没用过也闻讯过吗?毕竟是响当当的Jake
Wharton出品的事物。若是没用过的话可以看看这里,里面固然是讲的Annotation,可是例子就是用注明加反射已毕的低档的Butterknife。哈哈!用法里面大约也说了下。

所以设计师最消极的就是灵感的管制——难题来了:

Butterknife原理

讲到butterknife的法则。那里不得不提一下相似那种注入框架都是运作时注解,即宣称注明的生命周期为RUNTIME,然后在运转的时候经过反射落成注入,那种艺术即使简易,可是那种艺术多多少少会有品质的损耗。那么有没有一种办法能一蹴而就那种性质的费用呢?
没错,答案自然是一对,那就是Butterknife用的APT(Annotation Processing
Tool)编译时解析技术。

APT大致就是你申明的笺注的生命周期为CLASS,然后继续AbstractProcessor类。继承这几个类后,在编译的时候,编译器会扫描所有带有你要处理的申明的类,然后再调用AbstractProcessor的process方法,对申明进行处理,那么大家就可以在拍卖的时候,动态变化绑定事件仍然控件的java代码,然后在运转的时候,直接调用bind方法成功绑定。
事实上那种艺术的好处是我们不要再度四随处写findViewById和onClick了,那些框架在编译的时候帮我们自动生成了那几个代码,然后在运转的时候调用就行了。

1、图片收集功效太低(一张一张另存?)
2、灵感收集太散(腾讯网收藏?推特收藏?ipad保存?台式机保存?)
3、分类太难为(一张图片可能既属于“UI”,也属于“流行乐”,还属于“移动设计”)
4、使用的时候找不到(不确定在哪层目录,预览慢)……

源码解析

地点讲了那么多,其实都不如直接解析源码来得直接,上边大家就一步一步来探索大神怎么着落成Butterknife的吗。

获得源码的首先步是从大家调用的地点来突破,那我们就来看看程序里面是怎么样调用它的吗?

 @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.setDebug(true);
    ButterKnife.bind(this);

    // Contrived code to use the bound fields.
    title.setText("Butter Knife");
    subtitle.setText("Field and method binding for Android views.");
    footer.setText("by Jake Wharton");
    hello.setText("Say Hello");

    adapter = new SimpleAdapter(this);
    listOfThings.setAdapter(adapter);
  }

下面是github上给的例证,我们向来就从
ButterKnife.bind(this)入手吧,点进入看看:

  public static Unbinder bind(@NonNull Activity target) {
    return bind(target, target, Finder.ACTIVITY);
  }

咦?我再点:

  static Unbinder bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
      return viewBinder.bind(finder, target, source);
    } catch (Exception e) {
      throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
  }

行吗,bind方法主要就是获得大家绑定的Activity的Class,然后找到那几个Class的ViewBinder,最终调用ViewBinder的bind()主意,那么难点来了,ViewBinder是个怎么样鬼???我们开拓
findViewBinderForClass()方法。

 @NonNull
  private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
      throws IllegalAccessException, InstantiationException {
    ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
      return viewBinder;
    }
    String clsName = cls.getName();
    try {
      Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
      viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
    } catch (ClassNotFoundException e) {
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }

那里我去掉了部分Log新闻,保留了首要代码,上面的BINDERS是一个保留了Class为key,Class$$ViewBinder为Value的一个LinkedHashMap,重即使做一下缓存,提升下次再来bind的质量。
在第10行的时候,clsName
是咱们传入要绑定的Activity类名,那里一定于拿到了Activity$$ViewBinder本条事物,那几个类又是什么玩意儿?其实从类名可以看出来,相当于Activity的一个里边类,这时候大家就要问了,大家在用的时候没有申明那些类呀???从哪个地方来的?
不要方,其实它就是大家在从前讲原理的时候说到的AbstractProcessor在编译的时候生成的一个类,大家前边再来看它,现在大家一连往下边分析。在第11行就用反射反射了一个viewBinder
实例出来。
恰好说了,这一个点子里面用linkhashMap做了下缓存,所以在15行的时候,就把刚刚反射的viewBinder作为value,Class作为key插足这几个LinkedHashMap,下次再bind那么些类的时候,就直接在第4行的时候取出来用,升高质量。

现今回去刚刚的bind方法,我们得到了这一个Activity的viewBinder,然后调用它的bind方法。咦?那就完了???大家再点进viewBinder的bind方法看看。

public interface ViewBinder<T> {
  Unbinder bind(Finder finder, T target, Object source);
}

怎么着,接口???什么鬼?刚刚不是new了一个viewBinder出来么?然后那里就调用了这一个viewBinder的bind方法,
不行,我要看一下bind究竟是哪些鬼!上面说了,Butterknife用了APT技术,那么那里的viewBinder应该就是编译的时候生成的,那么大家就反编译下apk。看看究竟生成了怎样代码:
上边大家就先用一个概括的绑定TextView的例证,然后反编译出来看看:

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.text_view)
    TextView textView;

    @OnClick(R.id.text_view)
     void onClick(View view) {
        textView.setText("我被click了");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        textView.setText("我还没有被click");
    }
}

源代码就那行几行,然后反编译看看:

源代码就多了一个类,MainActivity$$ViewBinder,打开看看:

public class MainActivity$$ViewBinder<T extends MainActivity>
  implements ButterKnife.ViewBinder<T>
{
  public void bind(ButterKnife.Finder paramFinder, final T paramT, Object paramObject)
  {
    View localView = (View)paramFinder.findRequiredView(paramObject, 2131492944, "field 'textView' and method 'onClick'");
    paramT.textView = ((TextView)paramFinder.castView(localView, 2131492944, "field 'textView'"));
    localView.setOnClickListener(new DebouncingOnClickListener()
    {
      public void doClick(View paramAnonymousView)
      {
        paramT.onClick(paramAnonymousView);
      }
    });
  }

  public void unbind(T paramT)
  {
    paramT.textView = null;
  }
}

还记得刚刚说的,反射了一个Class$$ViewBinder么?看那里的类名。现在理应懂了啊?它恰恰也是促成了ButterKnife.ViewBinder<T>接口,大家说了,在bind方法中,最终调用了ViewBinder的bind方法,先说下多少个参数paramFinder其实就是一个Finder,因为大家可以在Activity中拔取butterknife,也得以在Fragment和Adapter等中运用butterknife,那么在分化的地点采用butterknife,这么些Finder也就差异。在Activity中,其实源码
就是那样子的:

 ACTIVITY {
    @Override protected View findView(Object source, int id) {
      return ((Activity) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return (Activity) source;
    }
  }

有没有很熟谙???其实照旧用的findViewById,那么在Dialog和Fragment中,依据分化的地方,落成的格局各异。

那里的paramT和paramObject都是我们要绑定的Activity类,通过代码可以跟踪到。

回到上边的ViewBinder代码,首先调用了Finder的findRequiredView方法,其实那么些措施最终通过处理就是调用了findView方法,拿到对应的view,然后再赋值给paramT.textView,刚说了paramT就是非凡要绑定的Activity,现在懂了呢?那里经过
paramT.textView
那样的调用方式,表明了Activity中不可以把TextView设置为private,不然会报错,其实那里可以用反射来得到textView的,那里大约也是为了质量着想吧。最后setOnClickListener,DebouncingOnClickListener以此Listener其实也是促成了View.OnClickListener
方法,然后在OnClick里面调用了doClick办法。流程大致跟踪了两次。现在还留下最终一块了:

吐槽是从未尽头的,所以设计师们会设法找到各个笔记工具,不过,许多工具太过分复杂和巨大,对于苦逼设计师那种追求轻快高效的人流来说,使用那个工具本身也是一种负担…

Butterknife到底是何等在编译的时候生成代码的?

俺们来看一下它的ButterKnifeProcessor类:

Init方法:

  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
  }

ProcessingEnviroment参数提供不可胜言立见成效的工具类Elements,
Types和Filer。Types是用来拍卖TypeMirror的工具类,Filer用来成立生成扶助文件。至于ElementUtils嘛,其实ButterKnifeProcessor在运作的时候,会扫描所有的Java源文件,然后每一个Java源文件的每一个片段都是一个Element,比如一个包、类仍然措施。

 @Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();

    types.add(BindArray.class.getCanonicalName());
    types.add(BindBitmap.class.getCanonicalName());
    types.add(BindBool.class.getCanonicalName());
    types.add(BindColor.class.getCanonicalName());
    types.add(BindDimen.class.getCanonicalName());
    types.add(BindDrawable.class.getCanonicalName());
    types.add(BindInt.class.getCanonicalName());
    types.add(BindString.class.getCanonicalName());
    types.add(BindView.class.getCanonicalName());
    types.add(BindViews.class.getCanonicalName());

    for (Class<? extends Annotation> listener : LISTENERS) {
      types.add(listener.getCanonicalName());
    }

    return types;
  }

getSupportedAnnotationTypes()方法重若是指定ButterknifeProcessor是挂号给哪些申明的。大家可以见见,在源代码里面,作者一个一个地把Class文件加到这几个LinkedHashSet里面,然后再把LISTENERS也漫天加进去。

实质上任何类最关键的是process方法:

 @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();

      try {
        bindingClass.brewJava().writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
            e.getMessage());
      }
    }
    return true;
  }

其一法子的效果重大是扫描、评估和拍卖大家先后中的声明,然后生成Java文件。也就是后面说的ViewBinder。首先一进这些函数就调用了findAndParseTargets措施,我们就去探访findAndParseTargets格局到底做了什么:

  private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

      // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseBindView(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }

    Observable.from(topLevelClasses)
        .flatMap(new Func1<BindingClass, Observable<?>>() {
          @Override public Observable<?> call(BindingClass topLevelClass) {
            if (topLevelClass.hasViewBindings()) {
              // It has an unbinder class and it will also be the highest unbinder class for all
              // descendants.
              topLevelClass.setHighestUnbinderClassName(topLevelClass.getUnbinderClassName());
            } else {
              // No unbinder class, so null it out so we know we can just return the NOP unbinder.
              topLevelClass.setUnbinderClassName(null);
            }

            // Recursively set up parent unbinding relationships on all its descendants.
            return ButterKnifeProcessor.this.setParentUnbindingRelationships(
                topLevelClass.getDescendants());
          }
        })
        .toCompletable()
        .await();

    return targetClassMap;
  }

此间代码炒鸡多,我就不整保养出来了,只贴出来一部分,那么些艺术最后还用了rxjava的楷模。那些方式的关键的流水线如下:

  • 举目四望所有具有表明的类,然后依据这么些类的新闻生成BindingClass,最终生成以TypeElement为键,BindingClass为值的键值对。
  • 巡回遍历那个键值对,依据TypeElement和BindingClass里面的音讯变化对应的java类。例如AnnotationActivity生成的类即为Cliass$$ViewBinder类。

因为大家前边用的例证是绑定的一个View,所以大家就只贴了然析View的代码。好呢,那里遍历了所有带有@BindView的Element,然后对每一个Element进行辨析,也就进去了parseBindView本条主意中:

private void parseBindView(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
          BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();

    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass != null) {
      ViewBindings viewBindings = bindingClass.getViewBinding(id);
      if (viewBindings != null) {
        Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
        if (iterator.hasNext()) {
          FieldViewBinding existingBinding = iterator.next();
          error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
              BindView.class.getSimpleName(), id, existingBinding.getName(),
              enclosingElement.getQualifiedName(), element.getSimpleName());
          return;
        }
      }
    } else {
      bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    }

    String name = element.getSimpleName().toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    FieldViewBinding binding = new FieldViewBinding(name, type, required);
    bindingClass.addField(id, binding);

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

然后那里从一进去这些艺术到

  int id = element.getAnnotation(BindView.class).value();

都是在获得注脚音信,然后验证评释的target的品种是或不是一而再自view,然后上边这一行代码得到大家要绑定的View的id,再从targetClassMap里面取出BindingClass(那么些BindingClass是管理了拥有关于这几个申明的部分音信还有实例本身的音信,其实说到底是透过BindingClass来生成java代码的),倘诺targetClassMap里面不设有的话,就在

      bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);

此间生成一个,大家进去看一下getOrCreateTargetClass

private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
      TypeElement enclosingElement) {
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass == null) {
      String targetType = enclosingElement.getQualifiedName().toString();
      String classPackage = getPackageName(enclosingElement);
      boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
      String className = getClassName(enclosingElement, classPackage) + BINDING_CLASS_SUFFIX;
      String classFqcn = getFqcn(enclosingElement) + BINDING_CLASS_SUFFIX;

      bindingClass = new BindingClass(classPackage, className, isFinal, targetType, classFqcn);
      targetClassMap.put(enclosingElement, bindingClass);
    }
    return bindingClass;
  }

那其间其实很简单,就是赢得一些以此评释所修饰的变量的片段新闻,比如类名呀,包名呀,然后className此地就赋值成Class$$ViewHolder了,因为:

  private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";

下一场把那么些分析后的bindingClass参与到targetClassMap里面。

回来刚刚的parseBindView中,依据view的新闻生成一个FieldViewBinding,最终添加到上面生成的BindingClass实例中。那里基本到位明白析工作。最终回来findAndParseTargets中:

Observable.from(topLevelClasses)
        .flatMap(new Func1<BindingClass, Observable<?>>() {
          @Override public Observable<?> call(BindingClass topLevelClass) {
            if (topLevelClass.hasViewBindings()) {
              topLevelClass.setHighestUnbinderClassName(topLevelClass.getUnbinderClassName());
            } else {

              topLevelClass.setUnbinderClassName(null);
            }
            return ButterKnifeProcessor.this.setParentUnbindingRelationships(
                topLevelClass.getDescendants());
          }
        })
        .toCompletable()
        .await();

此间运用了rxjava,其实那里主要的工作是成立地点的绑定的保有的实例的解绑的关系,因为大家绑定了,最终在代码中依然会解绑的。那里预先处理好了那些涉及。因为那里要递归地成功解绑,所以用了flatmap,flatmap把每一个创设出来的
Observable 发送的风云,都汇聚到同一个 Observable 中,然后那一个 Observable
负责将那一个事件联合交由 Subscriber 。
唯独那有些事关到很多rxjava的东西,有趣味的童鞋去探访大神的写给android开发者的RxJava
详解
那篇小说,然后再来看那里就很自在了。

再次回到我们的process中,
现在条分缕析完了annotation,该生成java文件了,我再把代码贴一下:

 @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();

      try {
        bindingClass.brewJava().writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
            e.getMessage());
      }
    }

    return true;
  }

遍历刚刚收获的targetClassMap ,然后再一个一个地通过

bindingClass.brewJava().writeTo(filer);

来生成java文件。不过生成的java文件也是按照地点的新闻来用字符串拼接起来的,然则那么些工作在brewJava()中成就了:

  JavaFile brewJava() {
    TypeSpec.Builder result = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC)
        .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));
    if (isFinal) {
      result.addModifiers(Modifier.FINAL);
    }

    if (hasParentBinding()) {
      result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentBinding.classFqcn),
          TypeVariableName.get("T")));
    } else {
      result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
    }

    result.addMethod(createBindMethod());

    if (hasUnbinder() && hasViewBindings()) {
      // Create unbinding class.
      result.addType(createUnbinderClass());

      if (!isFinal) {
        // Now we need to provide child classes to access and override unbinder implementations.
        createUnbinderCreateUnbinderMethod(result);
      }
    }

    return JavaFile.builder(classPackage, result.build())
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

此处运用了java中的javapoet技术,不打听的童鞋能够传递到github上面,也是square的大手笔,这一个不在那篇小说的教学范围内,有趣味的童鞋可以去看望,很科学的开源项目。

最后经过writeTo(Filer filer)生成java源文件。

倘使对javapoet感兴趣的话,可以看看那篇小说,介绍javapoet的。

写了某些个小时终于写完了。假诺有荒唐欢迎指正(๑>؂<๑)

笔者明日为设计师们介绍一种神奇的灵感收集术!借助一个叫“方片”的浏览器插件,最大的风味就是
 

①简单 ②快

快到令人切齿!看看“方片”是怎么保存图片的吗。

“OK记”收集图片只需拖拽一下即可保存到互连网和本土硬盘

正如上图,方片的样子是“浏览器侧栏”,安装好插件后,可以每一日突显/关闭。当须求收集图片时,只需将图片向侧栏一拽,即可保存到自己的网络账号里去。

采访好的图样,以卡片的样式呈列,速度非凡快。按照方片官方的计算,平均收集一张图纸仅需0.13秒,是前满世界最快的微笔记工具。什么tumblr,dribble,behance,twitter,insgrame…可想而知看到好图,随意拖拽进去就好了。

那是一款无需学习的工具,是还是不是过分简短了?确实就是那样简单。

下载(电脑打开) http://funp.in

网上收集了那和多材料,手机上能看呢?

答案是:已支持 ios 和 android

扫码安装


进阶

1、怎么样将图片下载到我的电脑?(只需一步)
2、怎么样批量下载图片?(如故只需一步)
3、怎么样管理自己的灵感图片?(So easy)
4、如何让小伙伴看自己的灵感图片?(分享太爽了)

下载图片到地面

在插件菜单里勾选
“启用图片拖拽下载”,然后再收集图片看看?就是那般随便这么简单。如下图。

假使想一边拖拽收集图片,一边下载,只需打开右上角的
“启动图片拖拽下载”选项。当然,你也得以不启用(图片将仅保留在你的网络账号里)

当然,你势必想知道,那多少个曾经保存到侧栏里卡片里的灵感图片怎么下载呢?别担心,也只需轻点一下搞定。如下图。

点击侧栏里图片上的“箭头”图标,刹那间下载

=

管理和查阅你的图样

图表保存在该地,查找起来还真是花气力,不过保存在网站上,管理会不会太费事呢?
探望下边这几个示例

透过“OK记”的图片墙功效,查看和治本图片

管理,在其他网页上,都得以展开图片墙成效,来查看自己的图纸。好处是

a.
每张图片都会保留其原始音讯(图片源网址,收集的光阴,地方,设备),以便查找
b. 还足以为每个图标添加描述(图片有了语义后就可以查找)
c.
分类查看(连串就是你收集时所选的类,每张图片都得以被标记多少个分类标签)
d.
可以批量操作(比如选十张联合添加某个标签,或者批量享受到微信好友,或者和讯等)
e. 能够批量下载(见下载教程)

每张图片卡都足以管理

在图纸墙下可以批量管理

=

批量下载图片

有三种办法批量下载图片
1,从你曾经收集好的 “图片墙”
中批量下载,太简单了,多选,点“下载”图标,搞定。看看下图吧。

极速下载已经收藏的图纸

2,从某个网页上批量下载图片。那些不可以更简约了,还记得 “启用图片拖拽下载”
么?启用后,拖拽一批图片到侧栏试试… 如下图。

可以从其余网页上批量拖拽下载图片

享受您的灵感图片给工作伙伴们

有图不享,自己独爽…那是很劣质的行事哦,那怎么给小伙伴们吧,看看哪些享受呢

发觉并未,你选拔的图片,将会友善生成一张POST,可以由此博客园,微信,twitter等种种途径分享给爱人们。效用越发赞!

至于灵感收集的故事就写到这里,要有如何难点自己去雕饰吧。


PS1:
没悟出那样一个小课程居然引来广大赞,真是心旷神怡,更有同伴以身试法,立马收集女神并生成一张喷血福利贴!不用说哥没提示你——纸巾和流量准备好再点http://m.okay.do/image/share/347795fd31268ea5eecf4db1ee4a1edf297289b7?

PS2:
在网上还看到一个摄像版OK记的恶搞视频
http://www.acfun.tv/v/ac1571384

Post Author: admin

发表评论

电子邮件地址不会被公开。 必填项已用*标注