1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.databinding; 18 19import android.app.Activity; 20import android.support.annotation.Nullable; 21import android.view.InflateException; 22import android.view.LayoutInflater; 23import android.view.View; 24import android.view.ViewGroup; 25import android.view.ViewParent; 26 27/** 28 * Utility class to create {@link ViewDataBinding} from layouts. 29 */ 30public class DataBindingUtil { 31 private static DataBinderMapper sMapper = new DataBinderMapper(); 32 private static DataBindingComponent sDefaultComponent = null; 33 34 /** 35 * Prevent DataBindingUtil from being instantiated. 36 */ 37 private DataBindingUtil() {} 38 39 /** 40 * Set the default {@link DataBindingComponent} to use for data binding. 41 * <p> 42 * <code>bindingComponent</code> may be passed as the first parameter of binding adapters. 43 * <p> 44 * When instance method BindingAdapters are used, the class instance for the binding adapter 45 * is retrieved from the DataBindingComponent. 46 */ 47 public static void setDefaultComponent(DataBindingComponent bindingComponent) { 48 sDefaultComponent = bindingComponent; 49 } 50 51 /** 52 * Returns the default {@link DataBindingComponent} used in data binding. This can be 53 * <code>null</code> if no default was set in 54 * {@link #setDefaultComponent(DataBindingComponent)}. 55 * 56 * @return the default {@link DataBindingComponent} used in data binding. This can be 57 * <code>null</code> if no default was set in 58 * {@link #setDefaultComponent(DataBindingComponent)}. 59 */ 60 public static DataBindingComponent getDefaultComponent() { 61 return sDefaultComponent; 62 } 63 64 /** 65 * Inflates a binding layout and returns the newly-created binding for that layout. 66 * This uses the DataBindingComponent set in 67 * {@link #setDefaultComponent(DataBindingComponent)}. 68 * <p> 69 * Use this version only if <code>layoutId</code> is unknown in advance. Otherwise, use 70 * the generated Binding's inflate method to ensure type-safe inflation. 71 * 72 * @param inflater The LayoutInflater used to inflate the binding layout. 73 * @param layoutId The layout resource ID of the layout to inflate. 74 * @param parent Optional view to be the parent of the generated hierarchy 75 * (if attachToParent is true), or else simply an object that provides 76 * a set of LayoutParams values for root of the returned hierarchy 77 * (if attachToParent is false.) 78 * @param attachToParent Whether the inflated hierarchy should be attached to the 79 * parent parameter. If false, parent is only used to create 80 * the correct subclass of LayoutParams for the root view in the XML. 81 * @return The newly-created binding for the inflated layout or <code>null</code> if 82 * the layoutId wasn't for a binding layout. 83 * @throws InflateException When a merge layout was used and attachToParent was false. 84 * @see #setDefaultComponent(DataBindingComponent) 85 */ 86 public static <T extends ViewDataBinding> T inflate(LayoutInflater inflater, int layoutId, 87 @Nullable ViewGroup parent, boolean attachToParent) { 88 return inflate(inflater, layoutId, parent, attachToParent, sDefaultComponent); 89 } 90 91 /** 92 * Inflates a binding layout and returns the newly-created binding for that layout. 93 * <p> 94 * Use this version only if <code>layoutId</code> is unknown in advance. Otherwise, use 95 * the generated Binding's inflate method to ensure type-safe inflation. 96 * 97 * @param inflater The LayoutInflater used to inflate the binding layout. 98 * @param layoutId The layout resource ID of the layout to inflate. 99 * @param parent Optional view to be the parent of the generated hierarchy 100 * (if attachToParent is true), or else simply an object that provides 101 * a set of LayoutParams values for root of the returned hierarchy 102 * (if attachToParent is false.) 103 * @param attachToParent Whether the inflated hierarchy should be attached to the 104 * parent parameter. If false, parent is only used to create 105 * the correct subclass of LayoutParams for the root view in the XML. 106 * @param bindingComponent The DataBindingComponent to use in the binding. 107 * @return The newly-created binding for the inflated layout or <code>null</code> if 108 * the layoutId wasn't for a binding layout. 109 * @throws InflateException When a merge layout was used and attachToParent was false. 110 */ 111 public static <T extends ViewDataBinding> T inflate( 112 LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent, 113 boolean attachToParent, DataBindingComponent bindingComponent) { 114 final boolean useChildren = parent != null && attachToParent; 115 final int startChildren = useChildren ? parent.getChildCount() : 0; 116 final View view = inflater.inflate(layoutId, parent, attachToParent); 117 if (useChildren) { 118 final int endChildren = parent.getChildCount(); 119 final int childrenAdded = endChildren - startChildren; 120 if (childrenAdded == 1) { 121 final View childView = parent.getChildAt(endChildren - 1); 122 return bind(bindingComponent, childView, layoutId); 123 } else { 124 final View[] children = new View[childrenAdded]; 125 for (int i = 0; i < childrenAdded; i++) { 126 children[i] = parent.getChildAt(i + startChildren); 127 } 128 return bind(bindingComponent, children, layoutId); 129 } 130 } else { 131 return bind(bindingComponent, view, layoutId); 132 } 133 } 134 135 /** 136 * Returns the binding for the given layout root or creates a binding if one 137 * does not exist. This uses the DataBindingComponent set in 138 * {@link #setDefaultComponent(DataBindingComponent)}. 139 * <p> 140 * Prefer using the generated Binding's <code>bind</code> method to ensure type-safe inflation 141 * when it is known that <code>root</code> has not yet been bound. 142 * 143 * @param root The root View of the inflated binding layout. 144 * @return A ViewDataBinding for the given root View. If one already exists, the 145 * existing one will be returned. 146 * @throws IllegalArgumentException when root is not from an inflated binding layout. 147 * @see #getBinding(View) 148 */ 149 @SuppressWarnings("unchecked") 150 public static <T extends ViewDataBinding> T bind(View root) { 151 return bind(root, sDefaultComponent); 152 } 153 154 /** 155 * Returns the binding for the given layout root or creates a binding if one 156 * does not exist. 157 * <p> 158 * Prefer using the generated Binding's <code>bind</code> method to ensure type-safe inflation 159 * when it is known that <code>root</code> has not yet been bound. 160 * 161 * @param root The root View of the inflated binding layout. 162 * @param bindingComponent The DataBindingComponent to use in data binding. 163 * @return A ViewDataBinding for the given root View. If one already exists, the 164 * existing one will be returned. 165 * @throws IllegalArgumentException when root is not from an inflated binding layout. 166 * @see #getBinding(View) 167 */ 168 @SuppressWarnings("unchecked") 169 public static <T extends ViewDataBinding> T bind(View root, 170 DataBindingComponent bindingComponent) { 171 T binding = getBinding(root); 172 if (binding != null) { 173 return binding; 174 } 175 Object tagObj = root.getTag(); 176 if (!(tagObj instanceof String)) { 177 throw new IllegalArgumentException("View is not a binding layout"); 178 } else { 179 String tag = (String) tagObj; 180 int layoutId = sMapper.getLayoutId(tag); 181 if (layoutId == 0) { 182 throw new IllegalArgumentException("View is not a binding layout"); 183 } 184 return (T) sMapper.getDataBinder(bindingComponent, root, layoutId); 185 } 186 } 187 188 @SuppressWarnings("unchecked") 189 static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots, 190 int layoutId) { 191 return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId); 192 } 193 194 static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root, 195 int layoutId) { 196 return (T) sMapper.getDataBinder(bindingComponent, root, layoutId); 197 } 198 199 /** 200 * Retrieves the binding responsible for the given View. If <code>view</code> is not a 201 * binding layout root, its parents will be searched for the binding. If there is no binding, 202 * <code>null</code> will be returned. 203 * <p> 204 * This differs from {@link #getBinding(View)} in that findBinding takes any view in the 205 * layout and searches for the binding associated with the root. <code>getBinding</code> 206 * takes only the root view. 207 * 208 * @param view A <code>View</code> in the bound layout. 209 * @return The ViewDataBinding associated with the given view or <code>null</code> if 210 * view is not part of a bound layout. 211 */ 212 public static <T extends ViewDataBinding> T findBinding(View view) { 213 while (view != null) { 214 ViewDataBinding binding = ViewDataBinding.getBinding(view); 215 if (binding != null) { 216 return (T) binding; 217 } 218 Object tag = view.getTag(); 219 if (tag instanceof String) { 220 String tagString = (String) tag; 221 if (tagString.startsWith("layout") && tagString.endsWith("_0")) { 222 final char nextChar = tagString.charAt(6); 223 final int slashIndex = tagString.indexOf('/', 7); 224 boolean isUnboundRoot = false; 225 if (nextChar == '/') { 226 // only one slash should exist 227 isUnboundRoot = slashIndex == -1; 228 } else if (nextChar == '-' && slashIndex != -1) { 229 int nextSlashIndex = tagString.indexOf('/', slashIndex + 1); 230 // only one slash should exist 231 isUnboundRoot = nextSlashIndex == -1; 232 } 233 if (isUnboundRoot) { 234 // An inflated, but unbound layout 235 return null; 236 } 237 } 238 } 239 ViewParent viewParent = view.getParent(); 240 if (viewParent instanceof View) { 241 view = (View) viewParent; 242 } else { 243 view = null; 244 } 245 } 246 return null; 247 } 248 249 /** 250 * Retrieves the binding responsible for the given View layout root. If there is no binding, 251 * <code>null</code> will be returned. This uses the DataBindingComponent set in 252 * {@link #setDefaultComponent(DataBindingComponent)}. 253 * 254 * @param view The root <code>View</code> in the layout with binding. 255 * @return The ViewDataBinding associated with the given view or <code>null</code> if 256 * either the view is not a root View for a layout or view hasn't been bound. 257 */ 258 public static <T extends ViewDataBinding> T getBinding(View view) { 259 return (T) ViewDataBinding.getBinding(view); 260 } 261 262 /** 263 * Set the Activity's content view to the given layout and return the associated binding. 264 * The given layout resource must not be a merge layout. 265 * 266 * @param activity The Activity whose content View should change. 267 * @param layoutId The resource ID of the layout to be inflated, bound, and set as the 268 * Activity's content. 269 * @return The binding associated with the inflated content view. 270 */ 271 public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId) { 272 return setContentView(activity, layoutId, sDefaultComponent); 273 } 274 275 /** 276 * Set the Activity's content view to the given layout and return the associated binding. 277 * The given layout resource must not be a merge layout. 278 * 279 * @param bindingComponent The DataBindingComponent to use in data binding. 280 * @param activity The Activity whose content View should change. 281 * @param layoutId The resource ID of the layout to be inflated, bound, and set as the 282 * Activity's content. 283 * @return The binding associated with the inflated content view. 284 */ 285 public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId, 286 DataBindingComponent bindingComponent) { 287 // Force the content view to exist if it didn't already. 288 View decorView = activity.getWindow().getDecorView(); 289 ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content); 290 T binding = inflate(activity.getLayoutInflater(), layoutId, contentView, false, 291 bindingComponent); 292 activity.setContentView(binding.getRoot(), binding.getRoot().getLayoutParams()); 293 return binding; 294 } 295 296 /** 297 * Converts the given BR id to its string representation which might be useful for logging 298 * purposes. 299 * 300 * @param id The integer id, which should be a field from BR class. 301 * @return The name if the BR id or null if id is out of bounds. 302 */ 303 public static String convertBrIdToString(int id) { 304 return sMapper.convertBrIdToString(id); 305 } 306} 307